网络编程-套接字socket模块
套接字发展史及分类
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
TCP协议的套接字面向连接的套接字,即在通信前建立一条连接(TCP的三次握手和四次挥手),这种通信方式也被称为"虚电路"或"流套接字"。
1 ss = socket() #创建服务器套接字 2 ss.bind() #把地址绑定到套接字 3 ss.listen() #监听链接 4 inf_loop: #服务器无限循环 5 cs = ss.accept() #接受客户端链接 6 comm_loop: #通讯循环 7 cs.recv()/cs.send() #对话(接收与发送) 8 cs.close() #关闭客户端套接字 9 ss.close() #关闭服务器套接字(可选)
1 cs = socket() # 创建客户套接字 2 cs.connect() # 尝试连接服务器 3 comm_loop: # 通讯循环 4 cs.send()/cs.recv() # 对话(发送/接收) 5 cs.close() # 关闭客户套接字
假如端口被socket使用过,并且利用socket.close()来关闭连接,但此时端口还没有释放,要经过一个TIME_WAIT的过程之后才能使用,这是TNN的相当烦银的,为了实现端口的马上复用,可以选择setsockopt()函数来达到目的。(以下是网上找到的一篇文章的一小段相关例子,试用之后,相当有效果,特此提取出来收藏) 端口复用的实现,我在这里用Python举个TCP端口复用的例子,UDP套接字要做的完全一样。 import socket tcp1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#在绑定前调用setsockopt让套接字允许地址重用 tcp1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)#接下来两个套接字都也可以绑定到同一个端口上
TCP实例-远程执行命令
import subprocess from socket import * server = socket(AF_INET,SOCK_STREAM) server.bind(("127.0.0.1",8888)) server.listen(5) while True: conn,addr = server.accept() while True: try: cmd = conn.recv(1024) # if not cmd:break # 针对Linux服务端,当客户端断开连接时,服务器一直接到空内容 cmd = cmd.decode("utf-8") obj = subprocess.Popen(cmd,shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout = obj.stdout.read() stderr = obj.stdout.read() conn.send(stdout+stderr) except ConnectionResetError: break
from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(("127.0.0.1",8888)) while True: cmd = input(">>>").strip() if not cmd:continue client.send(cmd.encode("utf-8")) data = client.recv(1024) print(data.decode("gbk")) client.close()
粘包问题的产生
由接收方造成的粘包
由传输方造成的粘包
tcp协议中会使用Nagle算法来优化数据。发送时间间隔短,数据量小的包会一起发送,造成粘包
结局粘包问题的基本套路
import subprocess import struct from socket import * server = socket(AF_INET,SOCK_STREAM) # 创建套接字对象 server.bind(("127.0.0.1",8888)) server.listen(5) while True: conn,addr = server.accept() # 等待连接,返回的第一个参数是套接字对象,第二个参数是客户端信息 while True: try: cmd = conn.recv(1024) cmd = cmd.decode("utf-8") obj = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) # 调用模块,执行命令。并且收集命令执行的结果 stdout = obj.stdout.read() # 把正确结果读出,存储在变量里 stderr = obj.stdout.read() # 把正确结果读出,存储在变量里 # 第一步:制作报头: total_size = len(stdout)+len(stderr) header = struct.pack("i",total_size) #存储包含数据长度的固定字节的字节码 # 第二步:先发报头: conn.send(header) # 第三步:发送命令结果 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
import struct from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(("127.0.0.1",8888)) while True: cmd = input(">>>").strip() client.send(cmd.encode("utf-8")) # 第一步:收到报头 header = client.recv(4) # 已知报头长度为四个字节 total_size = struct.unpack("i",header)[0] # 拿到数据长度 # 第二步:收完整真实数据 recv_size = 0 res = b"" while recv_size < total_size: # 计数器小于报头传入的数据长度 recv_data = client.recv(1024) # 从操作系统拿数据 res += recv_data # 拼接到res recv_size += len(recv_data) # 计数器加上拿到的数据的长度 print(res.decode("gbk")) client.close()
初级解决粘包问题的弊端
import subprocess import struct # 完成报头中的数字转换为固定长度的字节码 import json # 序列化模块 将数据类型信息转化为字符串 from socket import * # 套接字模块 server = socket(AF_INET,SOCK_STREAM) server.bind(("127.0.0.1",8888)) server.listen(5) while True: conn,addr = server.accept() while True: try: cmd = conn.recv(1024) cmd = cmd.decode("utf-8") obj = subprocess.Popen(cmd,shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout = obj.stdout.read() stderr = obj.stderr.read() # 制作报头,报头里放数据大小,MD5,文件名 header_dict ={ "total_size":len(stdout)+len(stderr), "md5":"********", "filename":"文件名" } header_json = json.dumps(header_dict) # 序列化报头 header_bytes = header_json.encode("utf-8") # 将报头转码编程字节码 header_size = struct.pack("i",len(header_bytes)) # 将转码后的报头长度转换成固定长度的字节码 # 先发报头长度 conn.send(header_size) # 再发报头 conn.send(header_bytes) # 在发送真实数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
import struct import json from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(("127.0.0.1",8888)) while True: cmd = input(">>>").strip() if not cmd:continue client.send(cmd.encode("utf-8")) # 先收报头长度-已知长度 obj = client.recv(4) header_size = struct.unpack("i",obj)[0] # 得到了报头长度 # 接收报头,解出报头内容 header_bytes = client.recv(header_size) # 根据报头长度得到报头 header_json = header_bytes.decode('utf-8') # 报头转码成str header_dic = json.loads(header_json) # 反序列化的到字典 total_size = header_dic['total_size'] # 得到真实数据长度 # 3:循环收完整数据 recv_size = 0 res = b"" while recv_size < total_size: # 计数器小于报头传入的数据长度 recv_data = client.recv(1024) # 从操作系统拿数据 res += recv_data # 拼接到res recv_size += len(recv_data) # 计数器加上拿到的数据的长度 print(res.decode("gbk")) client.close()
UDP协议的套接字
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
tcp是基于数据流的,于是收发的消息不能为空(如果发送数据为空 服务端端会得不到内容 一直等待接受内容 客户端也会等待接收服务端发送的内容,),这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
udp套接字简单示例
from socket import * ss = socket() #创建一个服务器套接字 ss.bind() #绑定服务器套接字 inf_loop: #服务器无限循环 cs = ss.recvfrom()/ss.sendto() #对话(接收与发送) ss.close() #关闭服务器套接字
cs = socket() # 创建客户套接字 comm_loop: # 通讯循环 cs.sendto()/cs.recvfrom() # 对话(发送/接收) cs.close() # 关闭客户套接字
一个返回输入内容大写的c/s架构
from socket import * server=socket(AF_INET,SOCK_DGRAM) server.bind(('127.0.0.1',8083)) while True: data,client_addr=server.recvfrom(1024) # recvfrom 收到的内容第一个元素是对面发送的内容,第二个是对面的ip端口元组 print('客户端的数据: ',data) server.sendto(data.upper(),client_addr) #sendto 第一个参数是要发送内容,第二个是目标ip端口元组
from socket import * client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ').strip() client.sendto(msg.encode('utf-8'),('127.0.0.1',8083)) #sendto 第一个参数是要发送内容,第二个是目标ip端口元组 data,server_addr=client.recvfrom(1024) # recvfrom 收到的内容第一个元素是对面发送的内容,第二个是对面的ip端口元组 print(data.decode('utf-8')) print(server_addr)
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

浙公网安备 33010602011771号