Pythonでsocket通信する - HondaLab/Robot-Intelligence GitHub Wiki

Noblocking UDP socket通信

センサー値をプログラムで利用するなどの場合、できるだけリアルタイム性が要求されます. センサー値読み取りプログラムと,感覚運動写像プログラムなどを一体のひとつのプログラムにしてしまうと, 速度の遅いセンサーがある場合,全体の速度も落ちてしまい,リアルタイム性が失われてしまいます.

センサー値読み取りプログラムを独立させて、その値をUDP socket通信で感覚運動写像プログラムなどに送ること によって速度の低下を防ぐ,すなわちリアルタイム性を確保することが可能になります. 受け取り側がデータをNoblocking受信することで、速度(rate)を落とすことなく、最新のセンサー値を受け取ることができるのです.

このような通信方式の考え方はポーリングと呼ばれることもあります.

下記にそのプログラム例を示します.

プログラムは3つのコードから成り立っています.

  • UDP_Send / Recv クラス定義
  • send側
  • recv側

1番目のクラス定義と同時に,アドレスとポートをそこに記述しておいて,send側とrecv側でその共通ファイルを importすれば,自動的に齟齬が生じない通信プログラムを書くことが出来ます.

リアルタイム性を損なわないためには、recv側のrateが常にsend側よりも速いこと

である。

その逆の場合、sendされたデータがバッファに貯まるので、recv側では遅延したデータを受け取ることになる。 つまり時間遅れ(むだ時間)を生じる元となる。

何らかの理由で、recv側のrateが遅くなる場合には、send側にtime.sleepをいれて、rateを落とす必要がある。

UDP_Send / Recv クラス

UDP_SendとUDP_Recvという、2つのクラスを、import用のコードにまとめた。 このファイルを、send用のコードとrecv用のコード双方でimportして使う。

関数をつかっても、同じことは可能である。 クラスを用いると、コンストラクタでsocketオブジェクトをつくるので、その生成がインスタンス作成のときだけに限定される。 関数だと、呼び出されるたびに毎回socketオブジェクトをつくることになる。

socketオブジェクトの生成をインスタンス生成のときだけにすることで、send時のオーバーヘッド低減をすこしだけ期待した。

socket1a.py

#!/usr/bin/python3
# socket1a.py
# CC BY-SA Yasushi Honda Since 2018 12/25
# UDP socketを使ってデータの送受信をするためのクラス
# 使い方:送信側と受信側の双方で、このモジュールをインポートして使う。
# ex: import socket1a as sk

import socket

# アドレスとポート
# アドレスはIPアドレスを直接していしても良いが、
# /etc/hostsに登録してあるホスト名を用いることもできる
ROBOT_ADDR = '172.16.1.xxx' # 
# port番号は32768 -- 60999 がカスタム用途のプライベート番号(cf. /proc/sys/net/ipv4/ip_local_port_range)
MPU9150_PORT = 50001 # 慣性センサ用のポート番号

class UDP_Send(): # send list data to (addr,port)
   def __init__(self,addr,port):
      self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK.DGRAMとすることで、UDPを指定
      self.addr = addr
      self.port = port

   def send(self,list): 
      #print(list) 
      string=''
      num=len(list)
      i=0
      while i<num:
         string = string + str("%12.8f" % list[i]) 
         if i!=num-1:
           string=string+','
         i=i+1
      #print(string)

      self.sock.sendto(string.encode('utf-8'),(self.addr,self.port))
      return 0

class UDP_Recv(): # Recieve list data from (addr,port)
   def __init__(self,addr,port):
      self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
      self.sock.bind((addr,port))
      self.sock.setblocking(0) # ポーリングのためのNoblocking受信

   def recv(self):
      message = self.sock.recv(1024).decode('utf-8')
      #print(message)
      slist=message.split(',')
      #print(slist)
      a=[float(s) for s in slist]
      #print(a) 
      return a

send example

send側の例を示す。 インスタンスudp をつくって、そこで相手のアドレスとポートを指定する。

sendメソッドに送るdataを引数として渡す。 dataは数値のリストを前提としている。 下記の例では、サイズが1のリスト(乱数)を送っている。

sendのrateはrecvのrateを超えてはいけない

send側の注意としては、rateを上げすぎないことである。 i2cなどで、rateが自然に落ちる場合は問題ないが、recv側のrateを上回る危惧がある場合には、ループにtime.sleepを挟んで、rateを落とす。

send_example.py

import socket
import random
import time
import socket1a as sk

if __name__=='__main__':

   udp = sk.UDP_Send(sk.ROBOT_ADDR, sk.MPU9150_PORT)
   data=[1] # 送信データ用のリスト
   i=0 
   start=time.time()
   print("# 10秒間乱数をsendするテスト")
   while time.time()-start<10 :
      data[0] = random.randint(1,100000000)/10000000
      print("%5.1f %s" % (time.time()-start,data[0]))
      udp.send(data)
      time.sleep(0.10)
      i=i+1

recv example

受信(recv)側の注意点は以下のふたつ

  • recvのrateはsendのrateよりも高い必要があるが、上げすぎるとCPUを100%消費する。
  • Noblocking recvするために except(BlockingIOError, socket.error): を用いる

recv_example.py

# recv側のプログラム(Robot)はこの例のように
# exceptでBlockingIOErrorを処理しなければならない。
# recv側のloop rate が send側のloop rateよりも大きくなるようにtime.sleep
# を調節する。これにより、センサー値のリアルタイム性が確保される。
# recv側のtime.sleepを外すと、rateは最大となるが、ビジーループとなり、
# CPUを最大消費するので注意が必要。

import socket1a as sk
import time
import socket

if __name__=='__main__':

   udp = sk.UDP_Recv(sk.ROBOT_ADDR, sk.MPU9150_PORT)
   # ここでは、socket1a内に登録してあるアドレスとポートを用いたが
   # 別のものを指定してももちろん機能する
   data=[1]
   start = time.time()
   print("# 10秒間Noblocking recieve のテスト")
   while time.time()-start<10 :
      try:
         data = udp.recv()
         print("%8.4f %12.8f" % ((time.time()-start),data[0]))
         time.sleep(0.05)
      except (BlockingIOError,  socket.error):
         # Noblockingなので、まだ届いてない場合の処理
         #print("%5.1f no recv" % (time.time()-start))
         time.sleep(0.05)
         continue
      except OSError:
         break

注意点

recv側で指定するアドレスは自分のアドレス

上の例では,送信側も受信側も共通のアドレス sk.ROBOT_ADDR を利用した. 単一ホスト内でのソケット通信なので,この様に指定した.

もちろん異なるホスト間でもネットを介してソケット通信できる. その場合には送信側は相手のアドレスを指定するのに対してい,受信側は自分のアドレスを指定する. つまり,ソケット通信に使われるアドレスは常に受信側のアドレスだけということになる.

importファイルを共通にする

ここに挙げた例では、send側、recv側双方がsocket1a.py をimportすることで、簡潔な記述でsockeをつかったNoblocking受信が可能になった。 送信側と、受信側の双方が同じディレクトリ内のプログラムにならば、socket1a.pyは当然共通であるが、ネットワークを介して送受信するばあい、双方に同じsocket1a.pyを置いておく必要がある。

同じファイル名socket1a.pyなのに内容が異なるファイルをを双方に配置すると、バグの元なので注意が必要である。

polling関連ページ

参考URL

*http://blog.amedama.jp/entry/2017/03/29/080000 (Nonblocking) *http://memo.saitodev.com/home/python_network_programing/#id3 (UDP) *http://gihyo.jp/admin/serial/01/net_prac_tech/0007 TCPとUDPの違い説明