Python ベースのソケット ネットワーク通信 [1]

1. ソケットの原理

上司の知識を学んだ後は、メモを取るだけです
https://www.jianshu.com/p/066d99da7cbd
http://c.biancheng.net/view/2351.html

1.1 ソケットとは

コンピュータ通信の分野では、socket は「ソケット」と訳され、コンピュータ間で通信するための慣例または方法です。ソケットの協定により、コンピュータは
  他のコンピュータからデータを受信したり、他のコンピュータにデータを送信したりすることができます。 > 読み取りおよび書き込み書き込み/読み取り –> クローズ」モードで動作します。
  私の理解では、ソケットはこのモードの実装です。つまり、ソケットは特殊なファイルであり、一部のソケット関数はそのファイルに対する操作 (読み取り/書き込み IO、オープン、クローズ) です。
  Socket() 関数は整数のソケット記述子を返し、接続の確立やデータ送信などの後続の操作はすべてソケットを通じて実装されます。

Baidu百科事典:ソケット (ソケット) は、アプリケーションがデータを送受信できる抽象化レイヤーであり、ファイルのように開いたり、読み取ったり、書き込んだり、閉じることができます。ソケットを使用すると、アプリケーションはネットワークに I/O を挿入し、ネットワーク上の他のアプリケーションと通信できるようになります。ネットワークソケットはIPアドレスとポートの組み合わせです。

Unix/Lunix の 1.2 ソケット

UNIX/Linuxシステムでは、さまざまなハードウェアの動作を統一し、インタフェースを簡素化するために、異なるハードウェアデバイスもファイルとしてみなします。これらのファイルに対する操作は、ディスク上の通常のファイルに対する操作と同等です。

UNIX/Linux ではすべてがファイルである、という多くの専門家の言葉を聞いたことがあるかもしれません。あの人は正しかった。

UNIX/Linux では、開かれているファイルを表現し区別するために、各ファイルに整数の ID が割り当てられ、この ID をファイル記述子 (File Descriptor) と呼びます。たとえば、通常、
0 は標準入力ファイル (stdin) を示すのに使用され、それに対応するハードウェア デバイスはキーボードです。
通常、1 は標準出力ファイル (stdout) を示すのに使用され、それに対応するハードウェア デバイスはモニタです。

UNIX/Linux プログラムは、あらゆる形式の I/O を実行するときに、ファイル記述子に対して読み取りまたは書き込みを行います。ファイル記述子は、開いているファイルに関連付けられた単なる整数であり、その背後には、ハードディスク上の通常のファイル、FIFO、パイプ、端末、キーボード、モニター、さらにはネットワーク接続が存在する可能性があります。

ネットワーク接続もファイルであり、ファイル記述子もあることに注意してください。あなたはこの文を理解する必要があります。

socket() 関数を使用してネットワーク接続を作成したり、ネットワーク ファイルを開くことができます。socket() の戻り値はファイル記述子です。ファイル記述子を使用すると、一般的なファイル操作関数を使用してデータを転送できます。たとえば、
リモート コンピュータからデータを読み取るには read() を使用し、
リモート コンピュータにデータを書き込むには write() を使用します。

ご存知のように、socket() を使用して接続を作成する限り、残りはファイル操作です。ネットワーク プログラミングは非常に簡単です。

Windows の 1.3 ソケット

Windows にも「ファイル記述子」に似た概念がありますが、通常は「ファイル ハンドル」と呼ばれます。したがって、このチュートリアルでは、Windows プラットフォームを指す場合は「ハンドル」を使用し、Linux プラットフォームを指す場合は「記述子」を使用します。

UNIX/Linuxと異なり、Windowsではソケットとファイルが区別され、Windowsではソケットをネットワーク接続として扱うため、ソケット専用のデータ転送関数を呼び出す必要があり、通常のファイルの入出力関数は無効となります。

2. プロセスはネットワーク内でどのように通信するのか

2.1、ローカルプロセス間通信

a. メッセージパッシング (パイプライン、メッセージキュー、FIFO)
  b. 同期 (ミューテックス、条件変数、読み書きロック、ファイルおよび書き込みレコードロック、セマフォ)
  c. 共有メモリ (匿名および名前付き、例: チャネル )
  d. リモートプロシージャコール(RPC)

2.2 プロセスはネットワーク内でどのように通信するか

ネットワーク内でプロセスがどのように通信するかを理解するには、次の 2 つの問題を解決する必要があります:
  a. ホストをどのように識別するか、つまり、通信しようとしているプロセスがどのホストで実行されているかを決定する方法。
  b. PID 識別を通じてローカルで一意のプロセスを識別するにはどうすればよいですか? ネットワーク内でそれを識別するにはどうすればよいですか?
解決策:
  a. TCP/IP プロトコル ファミリは、この問題の解決に役立ちました。ネットワーク層の「IP アドレス」は、ネットワーク内のホストを一意に識別できます。 b. トランスポート層の「プロトコル + ポート」は、ホストを一意に識別できます
  。ホスト (プロセス) 内のアプリケーション プログラムであるため、トリプレット(IP アドレス、プロトコル、ポート) を使用してネットワークのプロセスを識別でき、ネットワーク内のプロセス通信ではこのマークを使用して他のプロセスと対話できます。

3. ソケット通信の分類

トリプレット [IP アドレス、プロトコル、ポート] は、ネットワーク内のプロセス間でネットワーク間の通信に使用できます。ソケットは、トリプレットを使用してネットワーク通信を解決するミドルウェア ツールです。現時点では、ほとんどすべてのアプリケーションがソケットを使用しています。一般的に使用される 2 つのアプリケーションがあります。ソケット通信のデータ送信方式:
a. SOCK_STREAM: TCP プロトコルに対応し、コネクション型のデータ送信方式を示します。データはエラーなく別のコンピュータに到達でき、破損または紛失した場合でも再送信できますが、比較的時間がかかります。一般的な http プロトコルは、データの正確性を保証する必要があるため、SOCK_STREAM を使用してデータを送信します。そうしないと、Web ページを正常に解析できません。
  b. SOCK_DGRAM: UDP プロトコルに対応し、コネクションレス型のデータ送信方式を示します。コンピュータはデータを送信するだけで、データの確認はしないため、送信中にデータが破損したり、他のコンピュータに届かなかったりした場合、修復する方法がありません。つまり、データが間違っている場合は、間違っているので再送信することはできません。SOCK_DGRAM は検証作業が少ないため、SOCK_STREAM よりも効率的です。
  例: QQ ビデオ チャットと音声チャットでは、通信の効率を第一に確保し、遅延を最小限に抑える必要があるため、データの正確性は二の次であり、データの一部が失われたとしても、SOCK_DGRAM を使用してデータを送信します。ビデオとオーディオは引き続き使用できます。 通常の分析 (ノイズまたはせいぜいノイズ) は通信品質に大きな影響を与えません。
  
ソケット プログラミングは TCP および UDP プロトコルに基づいており、それらの階層関係は次の図に示されています。
ここに画像の説明を挿入

図 Socket() 関数

ここに画像の説明を挿入

4. ソケット (TCP) 接続確立のための 3 ウェイ ハンドシェイク

Socket の機能は、接続の確立、データの送信、データの受信の 3 つに簡略化されています。次のリンクは接続を確立するプロセスです
http://c.biancheng.net/view/2351.html

ソケットバッファ

各ソケットが作成された後、入力バッファーと出力バッファーの 2 つのバッファーが割り当てられます。

write()/send() は、すぐにネットワークにデータを送信するのではなく、まずデータをバッファに書き込み、次に TCP プロトコルによってバッファからターゲット マシンにデータを送信します。データがバッファに書き込まれると、データがターゲット マシンに到達したかどうかに関係なく、またデータがいつネットワークに送信されたかに関係なく、関数は正常に戻ることができます。これらの処理は TCP プロトコルが処理します。

TCP プロトコルは write()/send() 関数から独立しています。データはバッファに書き込まれるとすぐにネットワークに送信される場合もあれば、バッファ内でバックログが継続してデータが複数回書き込まれる場合もあります。これここに画像の説明を挿入
、その時点のネットワーク状況、現在のスレッドがアイドル状態であるかどうかなど、多くの要因によって決まり、プログラマによって制御されるものではありません。

read()/recv() 関数についても同様で、ネットワークから直接ではなく入力バッファから読み取ります。

これらの I/O バッファーのプロパティは次のように整理できます。

  1. I/O バッファは各 TCP ソケットに個別に存在します。
  2. I/O バッファはソケットの作成時に自動的に生成されます。
  3. ソケットが閉じられている場合でも、出力バッファに残っているデータの送信を続けます。
  4. ソケットを閉じると、入力バッファ内のデータが失われます。

通常の状況では、デフォルトのバッファのサイズは重要ではありませんが、次のコードを使用して表示および変更することもできます。

sock.getsockopt()        # 获取缓冲区大小
sock.setsockopt()         #更改缓冲区大小

送受信機能の特徴:

受信機能:

  1. 確立されたリンクのもう一方の端が切断された場合、recv はすぐに空の文字列を返します。

  2. recv は受信バッファからコンテンツを取得し、バッファが空のときにブロックします。

  3. recv が一度にバッファーの内容を受け入れることができない場合、次の実行を自動的に受け入れます。

送信機能:

  1. 送信のもう一方の端が存在しない場合、Pipe Broken 例外が生成されます。

  2. send は送信バッファからコンテンツを送信し、バッファがいっぱいになるとブロックします。

これは、TCP ソケットのブロック モードです。いわゆるブロッキングとは、前のアクションが完了せず、同期を維持するために、前のアクションが完了するまで次のアクションが一時停止されることを意味します。デフォルトでは、TCP ソケットはブロッキング モードになっており、これは最も一般的なモードでもあります。使用済み。

TCPプロトコルのスティッキーパケット問題

ソケットバッファとデータの転送処理においては、データの受信と送信は無関係であることがわかり、read()/recv()関数は、データが何度送信されても​​、可能な限り多くのデータを受信します。つまり、read()/recv() と write()/send() の実行時間は異なる場合があります。
たとえば、write()/send() が 3 回繰り返され、毎回文字列 "abc" が送信され、ターゲット マシン上の read()/recv() は "abc" を 3 回受信し、毎回 "abc" を受信します。 ; 2回受信、1回目は「abcab」、2回目は「cabc」; 文字列「abcabcabc」を1回受信することも可能です。

クライアントから毎回学生の学生IDを送信し、サーバーに学生の名前、住所、成績などの情報を返すようにしたいとしますが、このとき問題が発生し、サーバーが学生の学生IDを識別できない可能性があります。たとえば、最初に 1 が送信され、2 回目に 3 が送信された場合、サーバーはそれを 13 として扱う可能性があり、返される情報は明らかに間違っています。

これは、データの「スティッキー パケット」の問題であり、クライアントから送信された複数のデータ パケットが 1 つのデータ パケットとして受信されます。データの無制限性としても知られる read()/recv() 関数は、データ パケットの開始マーカーまたは終了マーカーを認識せず (実際には、開始マーカーまたは終了マーカーはありません)、それらを連続データとしてのみ処理します。ストリーム。

ソケットを本当に閉じます

close メソッドは接続のリソースを解放できますが、すぐには解放できません。すぐに解放したい場合は、閉じる前に shutdown メソッドを使用してください。shutdown メソッドは通信モードを実現するために使用されます。モードは 3 つあります。SHUT_RD は受信側を閉じます
。メッセージ チャネル、SHUT_WR が閉じます 送信メッセージ チャネル、SHUT_RDWR 両方のチャネルが閉じられます。
つまり、接続を閉じたい場合は、最初にすべてのチャネルを閉じてから、解放時に接続します。上記の 3 つの静的変数は、デジタル定数に対応します。 、1、2

self.tcpClient.shutdown(2)      #关闭消息发送通道
self.tcpClient.close()            #关闭套接字连接

Pythonソケットプログラミング例1 - テキスト送信

クライアント:

#port = str(input('please input sever port:'))
host = '192.168.2.107'    #客户端连接到服务器的ip
port = 5270  #端口
sever_address = (host, port)     #元组定义服务器地址,用于作为socket.connect()函数参数 连接到服务器
text_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     #创建一个socket对象为text_client

text_client.connect(sever_address)       #连接到服务器
succeed_flag='cok'                       #设置消息发送成功标志
text = 'connect succeed'
while True :
    try:
        text_client.send(text.encode())  # 发送文本数据,用 encode() 方法将str变为byte
        text = input('please input the message')
        receive_text = text_client.recv(1024).decode()
        print(receive_text)

    finally:
        print('send over!')

サーバ:

import socket

#可以手动输入本机ip地址,若有多个网口,服务器想从那个网口接收数据,就输入那个网口的ip
#hostname = socket.gethostname()  #可以用 .gethostname()函数来自动得到主机ip,不用手动输入了
#host = socket.gethostbyname(hostname)
host = '192.168.2.107'    #客户端连接到服务器的ip
port = 5270  #端口
sever_address = (host, port)     #创建元组作为 socket.bind()函数的输入,

text_sever = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     #创建一个socket对象为text_sever 为服务器
text_sever.bind(sever_address)    #.bind() 函数绑定端口,该服务器监听此端口
text_sever.listen(4)         #开启监听,同时接入数量最多为4

succeed_flag = 'sok'
while True :
    try:
        print(host)
        print('waiting connect')
        text_client_socket, text_client_address = text_sever.accept()              #accept() 函数,堵塞等待client的连接,连接到后才会执行下一条语句
        print(text_client_address[0] + 'is connected!')

        while True :
            receive_text = text_client_socket.recv(1024)  .decode()            #接收client发送的数据,数据最大为1024 ;此处可以看出接收用户数据测试
            print(receive_text)
            text_client_socket.send(succeed_flag.encode())                     #发送给client ok ,反馈自己确实接收到数据

    finally:
        print('work over!')

Pythonソケットプログラミング例2 - ビデオ送信

クライアント:

#-*- coding: UTF-8 -*-
import cv2                    #opencv2库,用于进行各种图像处理
import time
import socket                 #socket库,用于构建tcp/ip  通信

# 服务端ip地址
HOST = '192.168.2.102'      #字符串类型存储 host ip,tcp/ip通信服务器需要固定的ip与port
# 服务端端口号
PORT = 8080
ADDRESS = (HOST, PORT)       #元组方式存储ip与port

# 创建一个套接字,命名为tcpClient
tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     #socket.socket() 函数:socket.AF_INET:基于IPv4  ,socket.SOCK_STREAM对应TCP
# 连接远程ip
#tcpClient.bind(('192.168.3.122', 8080))
tcpClient.connect(ADDRESS)    #客户端向服务端发起连接。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误

#cap = cv2.VideoCapture('test1080.mp4')   #要发送的视频,如果是0为摄像头
cap = cv2.VideoCapture(0)
while True:
    # 计时
    start = time.perf_counter()         #计时器,第一次调用的时间存储在start里
    # 读取图像
    ref, cv_image = cap.read()           #返回第一个ref为true或false,表示是否读到了图像 ,第二个参数表示截取到一帧的图片数据,是一个三维数组
    # 压缩图像
    #cv2.imdecode()函数从指定的内存缓存中读取数据,并把数据转换(解码)成图像格式;主要用于从网络传输数据中恢复出图像。
    #cv2.imencode()函数是将图片格式转换(编码)成流数据,赋值到内存缓存中;主要用于图像数据格式的压缩,方便网络传输。
    img_encode = cv2.imencode('.jpg', cv_image, [cv2.IMWRITE_JPEG_QUALITY, 40])[1]        #第一个参数是压缩为什么格式,第二个参数是要压缩的数据源,最后一个参数是解码压缩参数,数字越大图片质量越好
    # 转换为字节流
    bytedata = img_encode.tostring()                     #将图像转换为字节流
    # 标志数据,包括待发送的字节流长度等数据,用‘,’隔开  ,发送的数据类型是 str
    flag_data = (str(len(bytedata))).encode() + ",".encode() + " ".encode()
    tcpClient.send(flag_data)                                  #客户端发送标志数据,服务器接收后知晓将要发送数据
    # 接收服务端的应答
    data = tcpClient.recv(1024)                #接收数据,数据以bytes类型返回,bufsize指定要接收的最大数据量为1024字节
    if ("ok" == data.decode()):               #接收到服务器返回'OK'后,发送全部图片数据,这里用decode进行了解码,因为socket传输字节流,对收到的字节解码才能得到字符串......
 #   if (data.decode()):
        # 服务端已经收到标志数据,开始发送图像字节流数据
        tcpClient.send(bytedata)
    # 接收服务端的应答
    data = tcpClient.recv(1024)
    if ("ok" == data.decode()):
        # 计算发送完成的延时
        print("延时:" + str(int((time.perf_counter() - start) * 1000)) + "ms")    #再次调用该计时函数,返回与上一次调用的时间间隔

サーバ:

#-*- coding: UTF-8 -*-
import socket
import cv2
import numpy as np

HOST = '192.168.2.102'
PORT = 8080
ADDRESS = (HOST, PORT)
# 创建一个套接字
tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地ip
tcpServer.bind(ADDRESS)
# 开始监听
tcpServer.listen(5)

while True:
    print("等待连接……")
    client_socket, client_address = tcpServer.accept()           #accept() 函数,堵塞等待客户端连接;反回(conn,address)二元元组,其中conn是一个通信对象,可以用来接收和发送数据。address是连接客户端的地址。
    print("连接成功!")
    try:                                                          #使用try   expect  便于处理异常
        while True:
            # 接收标志数据
            data = client_socket.recv(1024)
            if data:                                              #接收到的不为空就进入
                # 通知客户端“已收到标志数据,可以发送图像数据”
                client_socket.send(b"ok")                          #关于b"ok"      看https://www.delftstack.com/zh/howto/python/python-b-in-front-of-string/    ,这里是将ok变为byte
                # 处理标志数据
                flag = data.decode().split(",")                  #  strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列  ,这里将结尾的 '' 移除
                # 图像字节流数据的总长度
                total = int(flag[0])                         #flag[0]是第一个列表元素,flag是一个列表,返回第一个元素就是数据总长度的字符串形式,再用int()转为整数
                # 接收到的数据计数
                cnt = 0
                # 存放接收到的数据
                img_bytes = b""

                while cnt < total:
                    # 当接收到的数据少于数据总长度时,则循环接收图像数据,直到接收完毕
                    data = client_socket.recv(256000)                 #一次能接收的最大数据量为256000byte的数据
                    img_bytes += data                                #对总数据量计数
                    cnt += len(data)
                    print("receive:" + str(cnt) + "/" + flag[0])     #打印接收/需接收
                # 通知客户端“已经接收完毕,可以开始下一帧图像的传输”
                client_socket.send(b"ok")

                # 解析接收到的字节流数据,并显示图像
                img = np.asarray(bytearray(img_bytes), dtype="uint8")
                img = cv2.imdecode(img, cv2.IMREAD_COLOR)
                cv2.namedWindow("img", 0)
                cv2.resizeWindow("img", 1280, 720)             #重设置显示界面的大小
                cv2.imshow("img", img)                         #第一个参数是窗口的名字,第二个是图像
                cv2.waitKey(1)

            else:
                print("已断开!")
                break
    finally:
        client_socket.close()

Python ソケット プログラミングの例 3 - TCP プロキシ プロキシ サーバー

#https://www.youtube.com/watch?v=iApNzWZG-10

import socket
from threading import Thread
import os
#线程2

class Proxy2Server(Thread):
    #首先设置服务器连接(用_init_方法来构造)
    #参考https://www.cnblogs.com/ant-colonies/p/6718388.html
    def __init__(self, host, port):#如果没有在__init__中初始化对应的实例变量的话,导致后续引用实例变量会出错
        super(Proxy2Server,self).__init__()
        self.game = None #设置为连接用户的套接字,但是该套接字是由Game2Proxy线程创建的
        self.port = port
        self.host = host #连接服务器的ip和端口
        self.server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        self.server.connect((host,port))

    #在这个线程中执行的函数
    def run(self):
        #创建一个循环来执行数据处理和网络连接
        while True:
            data  = self.server.recv(4096)#最多接收4k的数据
            if data:
                #转发所有数据到用户
                print("[{}] <- {}")  #.format(self.port,data[:100].encode('hex'))#用作测试,可以打印出数据的流向
                self.game.sendall(data)




#线程1(监听用户是否与代理服务器连接)
class Game2Proxy(Thread):

    def __init__(self,host,port):
        super(Game2Proxy,self).__init__()
        self.server = None #设置为连接服务器的套接字,但是该套接字是由线程2创建的
        self.port = port
        self.host = host #连接用户的ip和端口
        sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
        sock.bind((host,port))
        sock.listen(1)#这些都是上面官方文档里面调用的例程实现的
        #等待用户的连接 
        self.game ,addr = sock.accept() #sock.accept接收套接字
        #当客户端连接之后我们将获得代理服务器与客户端通信的套接字,并将其分配给self.game,然后在下面的线程中利用永久循环来接收用户端的数据
    
    def run(self):
        while True: #死循环接收用户的数据
            data = self.game.recv(4096)#最大数据量4k
            if data:    #如果真的接收到了用户发送过来的数据,那麽我们会尝试将此数据转发到服务器的套接字,即另外一个线程的套接字
                #转发给服务器
                print("[{}] -> {}")       #.format(self.port,data[:100].encode('hex'))#用作测试,可以打印出数据的流向
                self.server.sendall(data)



#上面的两个线程创建完毕之后,需要为每一个线程提供对另外一个套接字的引用
#为此,我创建了一个更通用的类,命名为Proxy
class Proxy(Thread):
    def __init__(self,from_host,to_host,port):#如果没有在__init__中初始化对应的实例变量的话,导致后续引用实例变量会出错
        super(Proxy, self).__init__()
        self.from_host = from_host 
        self.to_host = to_host
        self.port = port 

    def run(self):
        while True:
            #print ("[proxy({})] setting up")
            print ("代理服务器设置完毕,等待设备接入...")
            #用户会连接到下面这个
            self.g2p = Game2Proxy(self.from_host, self.port) #运行我们创建的这个线程,它等待用户端连接到指定端口
            #如果代理服务器与用户建立连接之后,另外一个线程将建立到服务器的转发连接
            self.p2s = Proxy2Server(self.to_host, self.port)
            #print ("[proxy({})] connection established")
            print ("代理服务器已和设备连接,正在传输...")
            #现在两个线程都创建了套接字,我们接下来要做的就是交换他们
            self.g2p.server = self.p2s.server   #将与客户端建立的套接字转发给真实服务器
            self.p2s.game = self.g2p.game       #将服务器传回的套接字转发到客户端

            #线程设置完毕,现在我们来真正启动它
            self.g2p.start()
            self.p2s.start()


#写到这里的时候,唯一缺少的就是创建一个或多个代理线程,我们先从主服务器开始
master_server = Proxy('0.0.0.0', '192.168.2.222', 5555)
#监听自己所有本机端口3333,并将它转发到真实的服务器ip 192.168.178.54
master_server.start()   #启动

#_game_server = Proxy('0.0.0.0', '192.168.2.222', 5555)
#_game_server.start()

'''
#除此之外,客户端想要连接多个服务器的时候,我们可以启动多个代理(多分配几个不同端口即可)
for port in range(3000,3006):
    _game_server = Proxy('0.0.0.0','192.168.178.54',port)  
    _game_server.start()
#写到这里就已经可以工作了
'''

いくつかのアイテム

Python ソケットは、ファイルと md5 暗号化ファイルを送受信するための FTP クライアントとサーバーを実装します。

Pythonはsocketとopencvに基づいたビデオ伝送を実装します

おすすめ

転載: blog.csdn.net/qq_41224270/article/details/127916407