网络编程之socket模块以及粘包问题
2 为什么需要socket
在标准的OIS模型中并没有规定说必须有socket层,也就是说不使用socket也能完成通讯,是的,的确如此!
那为什么需要socket呢?一个字 懒,程序员都是懒的!
我们发现还没有开始实现应用程序逻辑,就需要花大把时间来实现各种协议,太特么费事儿了,就有人专门把协议中一堆复杂的事情进行了封装,于是socket就诞生了!
有了socket以后,无需自己编写代码实现三次握手,四次挥手,ARP请求,打包数据等等,socket已经封装好了,只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
3 socket的发展
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于大部通讯都是网络通讯,所以大部分时候使用AF_INET)
4 python中的socket
需明确:关于网络协议 和socket相关概念,对于所有编程语言都是一致的,区别仅仅是各编程语言的函数名称不同
# 1.导入socket模块 import socket # 2.创建socket对象 函数定义如下 socket.socket(socket_family,socket_type,protocal=0) #socket_family 可以是 AF_UNIX 或 AF_INET。 #socket_type 可以是 SOCK_STREAM表示TCP协议 或 SOCK_DGRAM表示UDP协议。 #protocol 一般不填,默认值为 0。 # 2.1获取TCP 套接字 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 或者 后面的参数都有默认值,可以不写,默认创建的是TCP协议socket tcpSock = socket.socket() # 2.2获取udp/ip套接字 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #由于 socket 模块中有太多的属性。可以使用'from module import *'语句。使用 'from socket import *',把 socket 模块里的所有属性都导入当前命名空间里了,这样能大幅减短代码。 #例如:tcpSock = socket(AF_INET, SOCK_STREAM)
**要明确一点:无论是客户端服务器端都使用的都是socket对象**
服务端套接字函数 s.bind() 绑定(主机,端口号)到套接字 s.listen() 开始TCP监听 s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来 客户端套接字函数 s.connect() 主动初始化TCP服务器连接 s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 公共用途的套接字函数 s.recv() 接收TCP数据 s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完) s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) s.recvfrom() 接收UDP数据 s.sendto() 发送UDP数据 s.getpeername() 连接到当前套接字的远端的地址 s.getsockname() 当前套接字的地址 s.getsockopt() 返回指定套接字的参数 s.setsockopt() 设置指定套接字的参数 s.close() 关闭套接字 面向锁的套接字方法 s.setblocking() 设置套接字的阻塞与非阻塞模式 s.settimeout() 设置阻塞套接字操作的超时时间 s.gettimeout() 得到阻塞套接字操作的超时时间
TCP下的客户端 服务端案例
# 服务器版本 # 当成一个买手机接听电话的过程 # 接收收据可能会出错 故放在try里尝试 import socket server= socket.socket() # 买手机 server.bind(('127.0.0.1',1999)) # 插卡 指定自己地址 和 端口 默认127.0.0.1是自身地址, 端口就是一个整数 注意:这时是元祖 server.listen() # 手机监听过程 while True: client_socket,client_addr = server.accept() # 接电话 print(client_socket) print(client_addr) buffer_size = 1024 # 设定缓存区域 while True: try: data =client_socket.recv(1024) # 收数据 if not data: client_socket.close() # 如果没有接收到数据 意味着对方下线 就关闭通道 break print('收到了数据',data.decode('utf-8')) # 接收到的都是二进制格式 需要转化 client_socket.send(data.upper()) # 发数据 except: # 上面运行错了就运行下面的 上面没错就运行上面的下面的步骤不走了 client_socket.close() # # 如果对方下线了 那服务器也应该关闭对应的客户端对象 break
# 相当于买手机通话 # 发送数据和接受数据容易出错 放在try里尝试 import socket client = socket.socket() # 买个手机 # 不需要插卡 是系统自动分配地址的 故不用插卡操作 client.connect(('127.0.0.1',1999)) # 连接到ip和端口 while True: msg = input('输入内容(q退出)').strip() if msg == 'q': break if not msg: continue # 发送的顺序要和服务器相反 client.send(msg.encode('utf-8')) # 发送数据 print('sending') data = client.recv(1024) # 接受数据 print(data.decode('utf-8')) client.close() # 运行完关闭通道
粘包问题
TCP流式协议, 数据之间没有分界, 就像水 一杯水和一杯牛奶倒在一起了!
UDP 用户数据报协议
粘包 仅发生在TCP协议中
- 发送端 发送的数据量小 并且间隔短 会粘
- 接收端 一次性读取了两次数据的内容 会粘
- 接收端 没有接收完整 剩余的内容 和下次发送的粘在一起
无论是那种情况,其根本原因在于 接收端不知道数据到底有多少
解决方案就是 提前告知接收方 数据的长度
解决方案
粘包问题的实质是接收方和发送方分别发送和接受的内容大小不一致, 那么让双方明确传输的长度就能能解决这问题 那么解决方案就是先传递长度 在传输内容
解决方案:
发送端
1:利用struct模块将目标数据转化为固定的字节
2:发送数据长度给接收端
3;发送数据
接收端
1;先接收长度数据 此时字节数是固定的
2:接收数据 (当发送端储传输的数据过大时,需要利用循环来进行分批次接收)
struct模块
struct模块是用来将整形的数字转化为固定长度的bytes的 返回的值是元祖 取值要按照索引0号位来取
另外 i 表示4个 字节 q表示8个字节 struct.pack 表示发送 struct.unpack 表示接收
import struct #struct是用来将整型的数字转成固定长度的bytes. import json header_dic={ 'total_size':32322, 'md5':'gdssfsfsdfsf', 'filename':'a.txt' } #1、将报头字典序列化。 header_json=json.dumps(header_dic) #2、将序列后的字典转成字节 header_bytes=header_json.encode('utf-8') #3、获取序列的字字典转成字节的个数 header_size=len(header_bytes) print(header_size) #4、将这个个数转成固字长度的字节表示 obj=struct.pack('i',header_size) print(obj,len(obj)) #、这个固定长度的字节经过反转后是一个元组。 res=struct.unpack('i',obj) #、通过按索取值就可等到报头字典长度。 header_size=res[0]
案例
# 客户端 import socket from cmd import smallTool import struct client = socket.socket() try: client.connect(("127.0.0.1",1688)) print("链接成功!") while True: msg = input("请输入要执行指令:").strip() if msg == "q": break if not msg: continue # 发送指令 # 先发长度 len_bytes = struct.pack("q",len(msg.encode("utf-8"))) client.send(len_bytes) # 在发指令 client.send(msg.encode("utf-8")) data = smallTool.recv_data(client) print(data.decode("GBK")) client.close() except ConnectionRefusedError as e: print("链接服务器失败了!",e) except ConnectionResetError as e: print("服务器挂了!", e) client.close()
# 服务端 import socket import subprocess import struct from cmd import smallTool server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(("127.0.0.1",1688)) server.listen() # back while True: # socket,addr一个元组 客户端的ip和port client,addr = server.accept() print("客户端链接成功!") # 循环收发数据 while True: try: cmd = smallTool.recv_data(client) if not cmd: break print(cmd) p = subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) # 不要先读err错误信息 它会卡主 原因不详 linux不会有问题 tasklist netstat - ano啥的 data = p.stdout.read() err_data = p.stderr.read() len_size = len(data) + len(err_data) print("服务器返回了: %s " % len_size) len_bytes = struct.pack("q",len_size) # 在发送真实数据前先发送 长度 client.send(len_bytes) # 返回的结果刚好就是二进制 # 发送真实数据 client.send(data + err_data) except ConnectionResetError as e: print("客户端了挂了!",e) break client.close() #server.close()