Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
套接字有两种(或者称为有两个种族),分别是基于文件型的AF_UNIX和基于网络型的AF_INET。
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
import socket server = socket.socket() # 默认就是基于网络的TCP传输协议 买手机 server.bind(('127.0.0.1', 8080)) # 绑定ip和port 插电话卡 server.listen(5) # 半连接池 开机(过渡) sock, address = server.accept() # 监听 三次握手的listen态 print(address) # 客户端地址 data = sock.recv(1024) # 接收客户端发送的消息 听别人说话 print(data) sock.send(b'hello client') # 给别人回话 sock.close() # 挂电话 server.close() # 关机server端
import socket client = socket.socket() # 买手机 client.connect(('127.0.0.1', 8080)) # 拨号 # 说话 client.send(b'hello server') # 听他说 data = client.recv(1024) print(data) client.close()client端
1.客户端校验消息不能为空,不然可能卡死
2.服务端需添加兼容性代码(针对不同操作系统可能出现的差异性)
3.服务端重启频繁报端口占用错误
from socket import SOL_SOCKET, SO_REUSEADDR # 模块 server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 在bind前加解决方法
4.客户端异常关闭服务端报错的问题
使用异常捕获
5.半连接池(listen)
设置可以等待的客户端数量
粘包指接收数据时,接收的不完全,当再次接收数据时会继续接收上一次未接收完的数据。或是在发送数据时,连续发送的多条数据接收时变成一条。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
粘包出现的原因是接收方不知道发送方到底发送了多少数据才产生的,要解决这个问题,就需要告诉接收方发送方到底发送了多少数据。
我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
struct模块可以把一个类型,如数字,转成固定长度的bytes,可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。发送到接收方后先接收struck的数据,从中获得字典的长度,再接收字典,从字典中取得真实数据的长度,最后接收数据。
这样的解包方式避免了数据过大struct无法将之转化的问题。
以一个上传文件的方式举例。
import json import struct import socket import os client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: data_path = r'D:\Linux\网络并发day01\视频' movie_list = os.listdir(data_path) for i, j in enumerate(movie_list, 1): print(i, j) choice = input('请输入要上传的电影编号').strip() if choice.isdigit(): choice = int(choice) if choice in range(1, len(movie_list)+1): # 获取文件名 movie_name = movie_list[choice - 1] # 拼接文件绝对路径 movie_path = os.path.join(data_path, movie_name) # 定义字典 data_dict = { 'file_name': movie_name, 'size': os.path.getsize(movie_path), 'info': '教学视频', } # 序列化字典 data_json = json.dumps(data_dict) # 制作报头 data_first = struct.pack('i', len(data_json)) # 发送字典报头 client.send(data_first) # 发送字典 client.send(data_json.encode('utf8')) # 发送真实数据 with open(movie_path, 'rb') as f: for line in f: client.send(line)server端
import socket import json import struct server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) while True: sock, address = server.accept() while True: # 1.先接收固定长度为4的字典报头数据 recv_first = sock.recv(4) # 2.解析字典报头 dict_length = struct.unpack('i', recv_first)[0] # 3.接收字典数据 real_data = sock.recv(dict_length) # 4.解析字典(json格式的bytes数据 loads方法会自动先解码 后反序列化) real_dict = json.loads(real_data) # 5.获取字典中的各项数据 data_length = real_dict.get('size') file_name = real_dict.get("file_name") recv_size = 0 with open(file_name, 'wb') as f: while recv_size < data_length: data = sock.recv(1024) recv_size += len(data) f.write(data)client端