Socket又称为套接字,它是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。
Socket也是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文(数据包),尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
二、TCP协议下的Socket
TCP是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
基础格式:
服务端
import socket sk = socket.socket() # 创建套接字 sk.bind(('127.0.0.1', 4444)) # 把地址绑定到套接字 sk.listen() # 监听链接 count, addr = sk.accept() # 接受客服端链接,count——client端与server端的链接信息,addr——client端IP地址 msg = count.recv(1024) # 接受客户端发送的信息,接受1024个字节 count.send(b'hi') # 像客户端发送信息(发送的信息必须是bytes数据类型) print(msg) # 打印接受信息 count.close() # 关闭客户端套接字 sk.close() # 关闭服务端套接字
客户端
import socket sk = socket.socket() # 创建套接字 sk.connect(('127.0.0.1', 4444)) # 尝试链接server端 sk.send(b'hello') # 发送信息 msg = sk.recv(1024) # 接受信息 print(msg) # 打印接受的信息 sk.close() # 关闭链接
sk——>是我自己的IP和端口
count——>是除了有我自己的IP和端口,还有连接我的客户端的IP和端口
自定义发送消息和同时连接多个客户端,并且和客户端多次交谈
服务端:
import socket sk = socket.socket() sk.bind(('127.0.0.1', 4444)) sk.listen() while True: # 为了能和多个客户端链接 count, addr = sk.accept() count.send('欢迎光临'.encode('utf-8')) # 先给客户端发送’欢迎光临‘ while True: # 为了能和一个客户端多交谈几次 msg1 = count.recv(1024).decode('utf-8') # 其次在接受客户端发送来的消息 if msg1.upper() == 'Q': break # 判断客户端是否要断开链接,结束循环就断开链接 print(msg1) # 不断开链接就打印消息 msg2 = input('>>') # 服务端输入要发送的消息 count.send(msg2.encode('utf-8')) # 发送消息 if msg2.upper() == 'Q': break # 服务端发送q就是结束循环,断开与客户端的链接 count.close() # 结束循环就会自动断开与客户端的链接
客户端:
import socket sk = socket.socket() sk.connect(('127.0.0.1', 4444)) msg3 = sk.recv(1024).decode('utf-8') # 第一次链接,接收服务端发送的’欢迎光临‘ print(msg3) # 并打印 while True: # 为了能和服务端多交谈 msg2 = input('>>') # 输入给服务端发送的消息 sk.send(msg2.encode('utf-8')) # 把消息发送给服务端 if msg2.upper() == 'Q': break # 判断客户端是否主动结束链接 msg1 = sk.recv(1024).decode('utf-8') # 接收服务的消息 if msg1.upper() == 'Q': break # 并判断服务端是否要结束链接 print(msg1) sk.close() # 结束循环就会执行close断开链接
三、UDP协议下的Socket
udp是无连接的,启动服务之后就可以直接接受消息,不需要提前建立链接
基础格式:
服务端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 创建套接字 sk.bind(('127.0.0.1', 4444)) # 把服务端地址绑定到套接字 count, addr = sk.recvfrom(1024) # 接收客户端的信息 print(count.decode('utf-8')) # 打印客户端发送的信息 msg = input('>>') # 输入发送信息 sk.sendto(msg.encode('utf-8'), addr) # 发送信息 sk.close() # 关闭服务器
客户端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 创建套接字 server = ('127.0.0.1', 4444) # 设置需要链接服务端的地址 msg = input('>>') sk.sendto(msg.encode('utf-8'), server) # 发送信息,带上服务端的地址 msg1 = sk.recv(1024) # 接收服务端的信息 print(msg1.decode('utf-8')) sk.close() # 断开链接
多个客户端和一个服务端连续交互
服务端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind(('127.0.0.1', 4444)) while True: # 为了能和一个客户端多次对话 count, addr = sk.recvfrom(1024) # 获取客户端发送的信息和客户端的地址信息 print(count.decode('utf-8')) # count是接收的信息,addr是客户端地址信息 msg = input('>>') sk.sendto(msg.encode('utf-8'), addr) # 通过获取到的客户端地址信息给客户端发信息
客户端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) server = ('127.0.0.1', 4444) while True: # 为了能和服务端多次对话 msg = input('>>') if msg.upper() == 'Q': break # 判断客户端是否主动断开链接 sk.sendto(msg.encode('utf-8'), server) # 发送消息 msg1 = sk.recv(1024).decode('utf-8') # 接收消息 if msg1.upper() == 'Q': break # 判断是否是服务端断开链接 print(msg1) sk.close() # 通过判断是否结束循环断开链接
四、粘包现象
粘包现象只出现在tcp协议中,因为tcp协议,多条消息之间没有边界,并且还有一大堆优化算法
情况一发送端的缓存机制:
两条消息都很短,发送的间隔时间也非常短,就会粘在一起
情况二接收端的缓存机制:
多条消息由于没有及时接收,而在接收方的缓存短堆粘一起导致的粘包
总结:
1、从表面上看,粘包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2、实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
解决粘包问题的本质:设置边界
五、粘包的解决方案
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知道,然后接收端接收指定字节的数据。
我们可以借助struct模块:
这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前,只要先接收这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止接受,就能刚好不多不少的接受完整的数据了。
Struct模块:
该模块可以把一个类型,如数字,转成固定长度的bytes类型:(四个字节)
import struct count = 452156 new_count = struct.pack('i', count) print(new_count) # 输出 b'<xe6x06x00'
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
发送时 | 接受时 |
---|---|
先发送struct转换好的数据长度4字节 | 先接收4个字节,并使用struct转换成数字来获取要接收到的数据 |
在发送数据 | 在按照长度接收数据 |
服务端:
import socket import struct sk = socket.socket() sk.bind(('127.0.0.1', 4444)) sk.listen() count, addr = sk.accept() # 先接收客户端经过转换后的信息长度字节,并转换成长度数字 msg3 = struct.unpack('i', count.recv(4))[0] # 在通过客户端发送的信息字节长度来,接收服务端发送的真实数据 msg4 = count.recv(msg3).decode('utf-8') print(msg4) msg1 = input('>>') # 将发送信息的字节长度数字,转换成四个字节的bytes类型,并发送到客户端 msg2 = struct.pack('i', len(msg1.encode('utf-8'))) count.send(msg2) # 在发送真实数据 count.send(msg1.encode('utf-8')) # 挥手断开链接 count.close() sk.close()
客户端:
import socket import struct sk = socket.socket() sk.connect(('127.0.0.1', 4444)) msg1 = input('>>') # 将发送信息的字节长度数字,转换成四个字节的bytes类型,并发送到服务端 msg2 = struct.pack('i', len(msg1.encode('utf-8'))) sk.send(msg2) # 在发送真实数据 sk.send(msg1.encode('utf-8')) # 先接收服务端经过转换后的信息长度字节,并转换成长度数字 msg3 = struct.unpack('i', sk.recv(4))[0] # 在通过服务端发送的信息字节长度来,接收服务端发送的真实数据 msg4 = sk.recv(msg3).decode('utf-8') print(msg4) # 挥手断开链接 sk.close()