socket编程加粘包现象
https://www.cnblogs.com/nulige/p/6235531.html
http://www.cnblogs.com/wupeiqi/articles/5040823.html
http://www.cnblogs.com/Eva-J/articles/8244551.html
http://www.cnblogs.com/linhaifeng/articles/6129246.html
一.Socket

sk.bind(address) s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。 sk.listen(backlog) 开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。 backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5 这个值不能无限大,因为要在内核中维护连接队列 sk.setblocking(bool) 是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。 sk.accept() 接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。 接收TCP 客户的连接(阻塞式)等待连接的到来 sk.connect(address) 连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 sk.connect_ex(address) 同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061 sk.close() 关闭套接字 sk.recv(bufsize[,flag]) 接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。 sk.recvfrom(bufsize[.flag]) 与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。 sk.send(string[,flag]) 将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。 sk.sendall(string[,flag]) 将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。 内部通过递归调用send,将所有内容发送出去。 sk.sendto(string[,flag],address) 将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。 sk.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s ) sk.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。 sk.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port) sk.fileno() 套接字的文件描述符
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求.
socket起源于Unix,而Unix/Linux基本哲学之一就是"一切皆文件",对于文件用[打开][读写][关闭]模式来操作.socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行操作(读/写IO,打开,关闭)
socket和file的区别:
file模块是针对某个指定文件进行[打开][读写][关闭]
socket模块是针对服务器端和客户端socket进行[打开][读写][关闭]
1.socket层
Socket是介于应用层和传输层之间
2.套接字的工作流程:
3.TCP协议和UDP协议
(1).TCP可靠的,面向对象的协议(如:打电话),传输效率低全双工通信(发送缓存或者接收缓存),面向字节流.使用TCP的应用:Web浏览器;电子邮件,文件传输程序.
(2).UDP不可靠的,无连接的服务,传输效率高(发送前时延小),一对一,一对多,多对一,多对多,面向报文,尽最大努力服务,无拥塞控制.使用UDP的应用:域名系统(DNS);视频流;IP语音(VoIP).
二.套接字的初使用
2.问题:有的人可能会遇到
解决方法:
#加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)
3.基于UDP协议的socket
udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接
(1).简单使用

import socket udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字 udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字 msg,addr = udp_sk.recvfrom(1024) print(msg) udp_sk.sendto(b'hi',addr) # 对话(接收与发送) udp_sk.close() # 关闭服务器套接字

import socket ip_port=('127.0.0.1',9000) udp_sk=socket.socket(type=socket.SOCK_DGRAM) udp_sk.sendto(b'hello',ip_port) back_msg,addr=udp_sk.recvfrom(1024) print(back_msg.decode('utf-8'),addr)
(2).具体示例:

import socket ip_port=('127.0.0.1',8081) udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server_sock.bind(ip_port) while True: qq_msg,addr=udp_server_sock.recvfrom(1024) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8'))) back_msg=input('回复消息: ').strip() udp_server_sock.sendto(back_msg.encode('utf-8'),addr)

import socket BUFSIZE=1024 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ '螳螂':('127.0.0.1',8081), '德邦':('127.0.0.1',8081), '剑圣':('127.0.0.1',8081), '剑豪':('127.0.0.1',8081), } while True: qq_name=input('请选择聊天对象: ').strip() while True: msg=input('请输入消息,回车发送,输入q结束和他的聊天: ').strip() if msg == 'q':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client_socket.recvfrom(BUFSIZE) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client_socket.close()
三.socket参数的详解
socket.socket(family = AF_INET,type = SOCK_STREAM,proto = 0,fileno = None)
创建socket对象的参数说明:
family |
地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS. (AF_UNIX域实际上是使用本地socket文件来通信) |
type |
套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一. SOCK_STREAM是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送 SOCK_DGRAM是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播消息 |
proto | 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一 |
fileno |
如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回. 与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的. 这可能有助于使用socket.close()关闭一个独立的插座 |
四.粘包
让我们基于TCP先制作一个远程执行命令的程序
这里引入一个subprocess模块
subprocess的目的就是启动一个新的进程并且与之通信
import subprocess
res=subprocess.Popen("dir",
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
print(res.stdout.read().decode("gbk"))
# 结果的编码是以当前所在的系统为准的,如果是Windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道中读一次结果
同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种现象就是粘包
1.基于TCP协议实现的粘包

from socket import * import subprocess ip_port=('127.0.0.1',8888) BUFSIZE=1024 tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) while True: conn,addr=tcp_socket_server.accept() print('客户端',addr) while True: cmd=conn.recv(BUFSIZE) if len(cmd) == 0:break res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() conn.send(stderr) conn.send(stdout)

import socket BUFSIZE=1024 ip_port=('127.0.0.1',8888) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) act_res=s.recv(BUFSIZE) print(act_res.decode('utf-8'),end='')
上述程序是基于TCP的socket,在运行时会发生粘包
2.基于UDP协议实制作一个远程执行命令的程序

from socket import * import subprocess ip_port=('127.0.0.1',9000) bufsize=1024 udp_server=socket(AF_INET,SOCK_DGRAM) udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) udp_server.bind(ip_port) while True: #收消息 cmd,addr=udp_server.recvfrom(bufsize) print('用户命令----->',cmd) #逻辑处理 res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() #发消息 udp_server.sendto(stderr,addr) udp_server.sendto(stdout,addr) udp_server.close()

from socket import * ip_port=('127.0.0.1',9000) bufsize=1024 udp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) err,addr=udp_client.recvfrom(bufsize) out,addr=udp_client.recvfrom(bufsize) if err: print('error : %s'%err.decode('utf-8'),end='') if out: print(out.decode('utf-8'), end='')
上述程序是基于UDP的socket,在运行时永远不会发生粘包
五.什么是粘包
须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来
首先需要掌握一个socket收发消息的原理
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的.
1.TCP两种情况下回发生粘包:
(1).发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据流很小,会合到一起,产生粘包)

from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()

import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('egg'.encode('utf-8'))
(2).接收方不及时接收缓冲区的包,造成多个包接收(客户发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(2) #一次没有收完整 data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()

import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello egg'.encode('utf-8'))
2.总结:
(1).从表面上看,粘包问题主要是因为发送方和接收方的缓冲机制,TCP协议面向流通信的特点
(2).实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
六.解决方案:
1.LOW版解决方案:

import socket,subprocess ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept() print('客户端',addr) while True: msg=conn.recv(1024) if not msg:break res=subprocess.Popen(msg.decode('utf-8'),shell=True,\ stdin=subprocess.PIPE,\ stderr=subprocess.PIPE,\ stdout=subprocess.PIPE) err=res.stderr.read() if err: ret=err else: ret=res.stdout.read() data_length=len(ret) conn.send(str(data_length).encode('utf-8')) data=conn.recv(1024).decode('utf-8') if data == 'recv_ready': conn.sendall(ret) conn.close()

import socket,time s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) length=int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) send_size=0 recv_size=0 data=b'' while recv_size < length: data+=s.recv(1024) recv_size+=len(data) print(data.decode('utf-8'))
为何low:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
2.高阶解决方案:
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
(1).首先引入struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes

import struct res=struct.pack("i","") print(res) print(len(res)) obj=struct.unpack("i",res) print(obj[0])
import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt
#为避免粘包,必须自定制报头
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值
#为了该报头能传送,需要序列化并且转为bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输
#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度
#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式
#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度
head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头
#最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)
3.使用struct解决粘包
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字.因此可以利用这个特点来预先发送数据长度
发送时 | 接收时 |
先发送struct转换好的数据长度4字节 | 先接收4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 | 再按照长度接收数据 |

import socket import json sock = socket.socket() # s.bind(address) 将套接字绑定到地址。 sock.bind(("127.0.0.1",8880)) sock.listen(5) while True: print("server is working...") conn,addr = sock.accept() while True: data = conn.recv(1024).decode("utf-8") # 接受套接字的数据。数据以字符串形式返回 file_info = json.loads(data) # 反序列化:将一个字符串格式的字典转换成一个字典 print("file_info",file_info) #{"action":"put","filename":"111.jpg","filesize":93605} # 文件信息 action = file_info.get("action") filename = file_info.get("filename") filesize = file_info.get("filesize") conn.send(b"1") # 接到数据返回1 # 接收到文件数据 with open("put/"+filename,"wb") as f: recv_data_length = 0 # 定义初始文件大小 while recv_data_length < filesize: data = conn.recv(1024) # 每次接收1024个字节 recv_data_length += len(data) # 初始大小+每次接收的文件大小 f.write(data) # 将每次接收的数据写入文件中 print("文件的总大小: %s,已成功接收%s" % (filesize,recv_data_length)) print("接收完成")

import socket import os import json sock = socket.socket() # 连接到address处的套接字。 sock.connect(("127.0.0.1",8880)) while True: cmd = input("请输入命令: ") #put 222.gif action,filename = cmd.strip().split(" ") filesize = os.path.getsize(filename) file_info = { "action":action, #命令 "filename":filename, #文件名 "filesize":filesize, #文件大小 } file_info_json = json.dumps(file_info).encode("utf-8") # 序列化将一个字典转换成字符串 sock.send(file_info_json) code = sock.recv(1024).decode("utf-8") if code == "1": # 发送文件数据 with open(filename,"rb") as f: for line in f: sock.send(line) else: print("服务器异常")
我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struct将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时 | 接收时 |
先发报头长度 | 先收报头长度,用struct取出来 |
再编码报头内容然后发送 | 根据取出的长度收取报头内容,然后解码,反序列化 |
最后发真实内容 | 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容 |

import socket,struct,json import subprocess phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() headers={'data_size':len(back_msg)} head_json=json.dumps(headers) head_json_bytes=bytes(head_json,encoding='utf-8') conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度 conn.send(head_json_bytes) #再发报头 conn.sendall(back_msg) #在发真实的内容 conn.close()

from socket import * import struct,json ip_port=('127.0.0.1',8080) client=socket(AF_INET,SOCK_STREAM) client.connect(ip_port) while True: cmd=input('>>: ') if not cmd:continue client.send(bytes(cmd,encoding='utf-8')) head=client.recv(4) head_json_len=struct.unpack('i',head)[0] head_json=json.loads(client.recv(head_json_len).decode('utf-8')) data_len=head_json['data_size'] recv_size=0 recv_data=b'' while recv_size < data_len: recv_data+=client.recv(1024) recv_size+=len(recv_data) print(recv_data.decode('utf-8')) #print(recv_data.decode('gbk')) #windows默认gbk编码
4.实例应用(向服务器上传文件)

import socket import struct import json import hashlib sock = socket.socket() sock.bind(("127.0.0.1",8800)) sock.listen(5) while True: print("server is working...") conn,addr = sock.accept() while True: # 接收json的打包长度 file_info_length_pack = conn.recv(4) file_info_length = struct.unpack("i",file_info_length_pack)[0] # 解包,包中内容应为(61,) # a = struct.unpack("i",file_info_length_pack) # print(a) # (61,) # 接收json字符串 file_info_json = conn.recv(file_info_length).decode("utf-8") file_info = json.loads(file_info_json) action = file_info.get("action") filename = file_info.get("filename") filesize = file_info.get("filesize") # 循环接收文件 md5 = hashlib.md5() with open("put/" + filename, "wb") as f: recv_data_length = 0 while recv_data_length < filesize: data = conn.recv(1024) recv_data_length += len(data) f.write(data) # MD5摘要 md5.update(data) print("文件总大小: %s,已成功接收%s" % (filesize,recv_data_length)) print("接收成功") conn.send(b"OJBK") print(md5.hexdigest()) md5_val = md5.hexdigest() client_md5 = conn.recv(1024).decode("utf-8") if md5_val == client_md5: conn.send(b'1') else: conn.send(b'0')

import socket import os import json import struct import hashlib sock = socket.socket() sock.connect(("127.0.0.1",8800)) while True: cmd = input("请输入命令: ") # put 222.gif action,filename = cmd.strip().split(" ") filesize = os.path.getsize(filename) file_info = { "action":action, "filename":filename, "filesize":filesize, } file_info_json = json.dumps(file_info).encode("utf-8") # 报头 ret = struct.pack("i",len(file_info_json)) print(ret) # 发送file_info_josn打包之后的长度 sock.send(ret) # 发送file_info_json字节串 sock.send(file_info_json) # 发送文件数据 md5 = hashlib.md5() with open(filename,"rb") as f: for line in f: sock.send(line) md5.update(line) data = sock.recv(1024) print(md5.hexdigest()) md5_val = md5.hexdigest() sock.send(md5_val.encode("utf-8")) is_valid = sock.recv(1024).decode("utf-8") if is_valid == "1": print("文件完整") else: print("文件上传失败")
七.关于hashlib模块的补充
import hashlib
md5=hashlib.md5()
md5.update(b"hello")
md5.update(b"yuan")
print(md5.hexdigest())
print(len(md5.hexdigest()))
#helloyuan: d843cc930aa76f7799bba1780f578439
# d843cc930aa76f7799bba1780f578439
分开加密和一起加密的结果一样

import hashlib md5=hashlib.md5() with open("ssh_client.py","rb") as f: for line in f: md5.update(line) print(md5.hexdigest()) # f.read()
八.socketserver模块的初次使用(多线程/多进程)
1.socketserver内部使用IO多路复用以及“多线程”和“多进程”,从而实现并发处理多个客户端请求的socket服务端。 即,每个客服端请求连接到服务器时,socket服务端都会在服务器上创建一个“线程”或“进程”专门负责处理当前客户端的所有请求

import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): # 字节类型 while 1: # 针对window系统 try: print("等待信息") data = self.request.recv(1024) # 阻塞 # 针对linux if len(data) == 0: break if data == b'exit': break response = data + b'SB' self.request.send(response) except Exception as e: break self.request.close() # 1 创建socket对象 2 self.socket.bind() 3 self.socket.listen(5) server=socketserver.ThreadingTCPServer(("127.0.0.1",8899),Myserver) server.serve_forever()

import socket sk = socket.socket() sk.connect(('127.0.0.1',8899)) while 1: name = input(">>>>:") sk.send(name.encode('utf-8')) # 字节 response = sk.recv(1024) # 字节 print(response.decode('utf-8'))