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”(数据报)。