Python3之socket编程

Python3之socket编程解决粘包问题

什么是粘包
当发送网络数据时,tcp协议会根据Nagle算法将时间间隔短,数据量小的多个数据包打包成一个数据包,先发送到自己操作系统的缓存中,然后操作系统将数据包发送到目标程序所对应操作系统的缓存中,最后将目标程序从缓存中取出,而第一个数据包的长度,应用程序并不知道,所以会直接取出数据或者取出部分数据,留部分数据在缓存中,取出的数据可能第一个数据包和第二个数据包粘到一起。

粘包解决方案
由于应用程序自己发送的数据可以进行打包处理,自己制作协议,对数据进行封装添加报头,然后发送数据部分。而报头必须是固定长度,对方接受时可以先接受报头,对报头进行解析,然后根据报头内的封装的数据的长度对数据进行读取,这样收取的数据就是一个完整的数据包

具体代码实现
struct模块的使用

被struct打包的数据会变成bytes格式,这样便于数据的网络传输

服务端

import socket
import struct
import subprocess

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8080))

phone.listen(5)
while 1:
    conn, addr = phone.accept()
    while 1:
        try:
            data = conn.recv(1024)
            res = subprocess.Popen(data.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = res.stdout.read()
            stderr = res.stderr.read()
            # 发送报头
            res = struct.pack('i', len(stdout) + len(stderr))
            conn.send(res)
            # 发送数据部分
            conn.send(stdout)
            conn.send(stderr)
        except Exception:
            break
    conn.close()
phone.close()

客户端

import socket
import struct

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))

while 1:
    cmd = input('请输入>>:').strip()
    if not cmd: continue
    phone.send(cmd.encode('utf-8'))
    # 接受报头
    res = phone.recv(4)
    # 对报头解压
    data_size = struct.unpack('i', res)[0]
    # 根据报头数据长度对数据进行接收
    recv_size = 0
    total_data = b''
    while recv_size < data_size:
        data_recv = phone.recv(1024)
        if (data_size - len(data_recv)) < 1024:
            left_data = phone.recv(data_size - len(data_recv))
            total_data += left_data
        total_data += data_recv
        recv_size += len(data_recv)

    data = phone.recv(1024)
    print(data.decode('gbk'))

phone.close()

Python之解读Socketserver & Tcpserver部分问题记录
在解析socketserver是如工作之前,我们先看看socektserver类的继承关系图:

请求类继承关系:
  在这里插入图片描述

server类继承关系:
  在这里插入图片描述

有了上面的继承关系图后,我们解析socketserver就轻松多了,下面,我们从代码开始,慢慢揭开socketserver面纱:

import socketserver
import struct, json, os

class FtpServer(socketserver.BaseRequestHandler):
    coding = 'utf-8'
    server_dir = 'file_upload'
    max_packet_size = 1024
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))

    def handle(self):
        print(self.request)
        while True:
            data = self.request.recv(4)
            data_len = struct.unpack('i', data)[0]
            head_json = self.request.recv(data_len).decode(self.coding)
            head_dic = json.loads(head_json)
            cmd = head_dic['cmd']
            if hasattr(self, cmd):
                func = getattr(self, cmd)
                func(head_dic)

    def put(self):
        pass

    def get(self):
        pass

if __name__ == '__main__':
    HOST, PORT = "localhost", 9999
    with socketserver.ThreadingTCPServer((HOST, PORT), FtpServer) as server:
        server.serve_forever()

Socket定长通讯读取消息长度头(java)

###一、前言
数据在网络传输时使用的都是字节流,Socket也不例外,所以我们发送数据的时候需要转换为字节发送,读取的时候也是以字节为单位读取。
那么问题就在于socket通讯时,接收方并不知道此次数据有多长,因此无法精确地创建一个缓冲区(字节数组)用来接收,在不定长通讯中,通常使用的方式时每次默认读取8*1024长度的字节,若输入流中仍有数据,则再次读取,一直到输入流没有数据为止。但是如果发送数据过大时,发送方会对数据进行分包发送,这种情况下或导致接收方判断错误,误以为数据传输完成,因而接收不全。
所以,大部分情况下,双方使用socket通讯时都会约定一个定长头放在传输数据的最前端,用以标识数据体的长度,通常定长头有整型int,短整型short,字符串Strinng三种形式。
###二、整型与短整型
int型的长度数据会以4个byte存放,所以可以读取前四个字节的数据,然后转换为int类型:

InputStream is = socket.getInputStream();
byte[] datalen = new byte[4];
is.read(datalen);//读取前四个字节数据存放到datalen中
int length = byteArrayToInt(datalen)//将字节数组转换为int型
byte[] data = new byte[length];
is.read(data);
String recvMsg = new String(data);//将获得数据转为字符串类型

byteArrayToInt的方法实现如下:

public static int byteArrayToInt(byte[] b){
	return b[3]&0xFF | (b[2]&0xFF) << 8 | (b[1]&0xFF) << 16 | (b[0]&0xFF) << 24 
}

当然还有一个更简单的方法,使用DataInputStream:

DataInputStream is = new DataInputStream(socket.getInputStream());
int datalen = is.readInt();//读取前四位字节并返回int类型数据
byte[] data = new byte[datalen];
is.readFully(data);
//使用阻塞的方式读取完整的datalen长度的数据
String recvMsg = new String(data);//将获得数据转为字符串类型

使用DataInputStream不但可以简化代码,而且它的readFully方法会以阻塞等待的形式读取完整datalen长度的数据,而上面的read方法有可能出现没有读取完整的情况,因此对于定长通讯推荐使用DataInputStream和readFully方法;
同样,对于短整型short,DataInputStream也带有对应的方法:

int datalen = is.readShort();//读取前两个字节数据并返回short型数据

同样也可以使用read读取前两位再转为int,再次不多赘述。
###三、字符串类型
字符串类型的好处是可以自行约定长度,将长度转为字符串,然后通过左补零的形式达到约定长度。假设约定定长头为8位,数据长度为1480,则这笔数据传输的前八位定长头为“00001480”。
处理方式为先读取约定长度字节,然后直接转为String类型,再转为int型

InputStream is = socket.getInputStream();
byte[] datalen = new byte[8];
//假设约定8位
is.read(datalen);
int length = Integer.parseInt(new String(datalen));//将字节数组转为字符串,再转为int类型
byte[] data = new byte[length];
is.read(data);
String recvMsg = new String(data);//将获得数据转为字符串类型

总结:
对于socket短链接通讯,建议使用定长头标识通讯内容长度,并且不宜发送过大的报文,以免通讯过程中产生内容丢失,比如网络丢包等情况。然后接收时尽量使用DataInputStream的readFully方法读取确定长度的数据

简单理解socket(AF_INET&SOCK_STREAM,SOCK_DGRAM)

套接字
在任何类型的通信开始之前,网络应用程序都必须创建套接字。

*套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信(Inter Process Communication,IPC)*

有两种类型的套接字:基于文件的和面向网络的。

基于文件的
家族名:AF_UNIX

(又名AF_LOCAL,在POSIX1.g标准中指定),它代表地址家族(addressfamily):UNIX。其他比较旧的系统可能会将地址家族表示成域(domain)或协议家族(protocolfamily),并使用其缩写PF而非AF。类似地,AF_LOCAL(在2000~2001年标准化)将代替AF_UNIX

面向网络的
家族名:AF_INET

或者地址家族:因特网。另一个地址家族AF_INET6用于第6版因特网协议(IPv6)寻址。此外,还有其他的地址家族,这些要么是专业的、过时的、很少使用的,要么是仍未实现的。在所有的地址家族之中,目前AF_INET是使用得最广泛的

总的来说,Python只支持AF_INET、AF_UNIX、AF_NETLINK和AF_TIPC家族

套接字地址:主机-端口对
做个比喻,套接字就像一个电话插孔,主机名和端口号就像区号和号码。
当程序之间需要通信时,需要知道对端的主机名(IP)和端口号。
有效的端口号范围为0~65535(小于1024的端口号预留给了系统)

面向连接的套接字与无连接的套接字
面向连接的套接字
TCP套接字的名字SOCK_STREAM。
特点:可靠,开销大。
在进行通信之前必须先建立一个连接,该连接的通信提供序列化的、可靠的和不重复的数据交付,而没有记录边界。这种类型的通信也称为虚拟电路或流套接字。
实现这种连接类型的主要协议是传输控制协议(缩写 TCP)
为了创建 TCP套接字,必须使用 SOCK_STREAM 作为套接字类型。

无连接的套接字
UDP套接字的名字SOCK_DGRAM
特点:不可靠(局网内还是比较可靠的),开销小。
与虚拟电路形成鲜明对比的是数据报类型的套接字,它是一种无连接的套接字。
在通信开始之前并不需要建立连接。此时,在数据传输过程中并无法保证它的顺序性、可靠性或重复性。数据报确实保存了记录边界,这就意味着消息是以整体发送的,而并非首先分成多个片段。
实现这种连接类型的主要协议是用户数据报协议(缩写 UDP)。为
了创建UDP套接字,必须使用SOCK_DGRAM作为套接字类型。
UDP套接字的SOCK_DGRAM名字来自於单词“datagram”(数据报)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章