基于tcp协议的socket编程
一、什么是Scoket
1、Socket介绍
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
注意:也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序,而程序的pid是同一台机器上不同进程或者线程的标识。
2、基于文件类型的套接字家族
AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
3、基于网络类型的套接字家族
AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以
大部分时候我么只使用AF_INET)
4、套接字工作流程
先从服务器端说起,服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
socket()是一个类,实例化对象后可以调用各种方法
class socket(_socket.socket): """A subclass of _socket.socket adding the makefile() method.""" __slots__ = ["__weakref__", "_io_refs", "_closed"] def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None): # For user code address family and type values are IntEnum members, but # for the underlying _socket.socket they're just integers. The # constructor of _socket.socket converts the given argument to an # integer automatically. _socket.socket.__init__(self, family, type, proto, fileno) self._io_refs = 0 self._closed = False
5、服务端套接字函数
方法
|
用途
|
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()
|
得到阻塞套接字操作的超时时间
|
方法
|
用途
|
s.fileno()
|
套接字的文件描述符
|
s.makefile()
|
创建一个与该套接字相关的文件
|
二、基于TCP协议的套接字编程(简单)
1、服务端
import socket # 1、实例化一个server对象 server = socket.socket() # server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 实例化得到socket对象 # server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 实例化得到socket对象 # 2、绑定ip+端口 server.bind(('127.0.0.1', 8000)) # 3、监听消息 server.listen(3) # 3 代表的是半连接池:可以等待的客户端的数量 # 4、接受消息 sock, addr = server.accept() # 代码走到这里会停住,等待接收客户端发来的消息。返回的是一个元组(tuple) # sock:代表的是当前客户端的连接对象 # addr:代表的是客户端的信息(ip+port) # 5、真正获取客户端发来的消息 data = sock.recv(1024) # 接受的最大数据字节数 print('客户端发来的消息是:%s' % data) # 6、服务端给客户端发消息 sock.send(b'this is server!') # 数据类型必须是字节型 # 7、断开与客户端之间的连接 sock.close() # 8、关闭server server.close()
半连接池(Half-Open Connection Pool)补充如下:
半连接池是一种用于管理连接状态的技术,旨在限制同时打开的未完成连接(半连接)的数量。
当服务器端接收到客户端的连接请求时,通常需要执行一系列操作来完成连接的建立,包括握手、认证等。在这些操作完成之前,连接处于半连接状态,也称为未完成连接。
半连接池的目的是通过限制半连接的数量,防止服务器资源被大量未完成的连接占用。通过控制半连接的数量,可以避免服务器过载和拒绝服务等问题。
可以通过设置socket
对象的listen()
方法的参数来实现半连接池。该参数表示服务器端可以同时接受的最大等待连接数,即半连接池的大小。
例如,server_socket.listen(3)
指定了半连接池的大小为3,服务器端最多能够同时处理3个未完成的连接。当半连接池已满时,服务器端会拒绝后续的连接请求。
需要注意的是,半连接池的大小应根据服务器的处理能力和资源限制来合理设置。设置过小的半连接池可能导致客户端连接被拒绝,而设置过大的半连接池可能导致服务器资源耗尽。
2、客户端
import socket # 1. 实例化对象 client = socket.socket() # 默认的参数就是基于网络的tcp socket # 2. 连接服务端 client.connect(('127.0.0.1', 8000)) # 3. 给服务端发送消息 client.send(b'this is client') # 4. 接受服务端发来的消息 data = client.recv(1024) print('服务端发来的消息:%s' % data) # 5. 断开连接 client.close()
三、基于TCP协议的套接字编程(循环版)
1、服务端
import socket # 1、实例化一个server对象 server = socket.socket() # server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 实例化得到socket对象 # server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 实例化得到socket对象 # 2、绑定ip+端口 server.bind(('127.0.0.1', 8001)) # 3、监听消息 server.listen(3) # 3 代表的是半连接池:可以等待的客户端的数量 while True: # 4、接受消息 sock, addr = server.accept() # 代码走到这里会停住,等待接收客户端发来的消息 # sock:代表的是当前客户端的连接对象 # addr:代表的是客户端的信息(ip+port) while True: try: # 5、真正获取客户端发来的消息 data = sock.recv(1024) # 接受的最大数据字节数 print('客户端发来的消息是:%s' % data) if len(data) == 0: break # 退出内层循环,重新接收一个sock客户端对象 # 6、服务端给客户端发消息 sock.send(b'this is server!') # 数据类型必须是字节型 except ConnectionResetError as e: print(e) break # 7、断开与客户端之间的连接 sock.close() # 8、关闭server server.close()
2、客户端(可以有1到n个客户端,取决于半连接池的大小设置)
import socket # 1. 实例化对象 client = socket.socket() # 默认的参数就是基于网络的tcp socket # 2. 连接服务端 client.connect(('127.0.0.1', 8001)) while True: res = input('请输入你要发送的消息:') if res == 'exit': break if res == '': continue # 3. 给服务端发送消息 client.send(res.encode('utf8')) # 4. 接受服务端发来的消息 data = client.recv(1024) print('服务端发来的消息:%s' % data) # 5. 断开连接 client.close()
四、多进程版(支持多客户端并发)
服务端
import socket import multiprocessing # 把向客户端发送数据、接收数据逻辑写到函数里面去,参数定义为每个传进来的sock 客户端对象 def handle_client(sock): while True: try: data = sock.recv(1024) # 接收客户端的数据 print('客户端发来的消息是:%s' % data) if len(data) == 0: # 判断客户端传过来的数据是否为空 break sock.send(b'this is server!') # 给客户端发消息 except ConnectionResetError as e: print(e) break sock.close() if __name__ == '__main__': server = socket.socket() server.bind(('127.0.0.1', 8001)) server.listen(4) # 循环部分的逻辑写客户端的接收、实例化、进程启动 while True: sock, addr = server.accept() p = multiprocessing.Process(target=handle_client, args=(sock,)) p.start() print(p.name) server.close()
客户端同上
服务端
import socketserver class MyTCPHandler(socketserver.BaseRequestHandler): def handle(self): # 接收数据 self.data = self.request.recv(1024).strip() # request 这里表示一个客户端 print('Rceived:', self.data) # 发送自定义数据 message = "This is server" # self.request.sendall(self.data.upper()) self.request.sendall(message.encode()) if __name__ == '__main__': HOST, PORT = 'localhost', 8888 server = socketserver.TCPServer((HOST, PORT), MyTCPHandler) # 永久服务 server.serve_forever()
客户端
import socket # 创建一个TCP socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect(('localhost', 8888)) # 发送数据 message = 'hello' sock.sendall(message.encode('utf8')) # 接收响应 response = sock.recv(1024) print('Response:', response.decode('utf8'))
1、服务端
import socket # 1、实例化一个server对象 server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # DGRAM 数据报协议UDP # 2、绑定ip+port server.bind(('127.0.0.1', 9000)) while True: data, client_addr = server.recvfrom(1024) # 接受客户端的信息(消息+ip:port) print('===>', data, client_addr) # 打印data 和 ip:port server.sendto(data.upper(), client_addr) # 给客户端发消息,复用上面的data转为大写 server.close()
2、客户端
import socket client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报协议-》UDP while True: msg = input('请输入你要发送的消息: ').strip() client.sendto(msg.encode('utf-8'), ('127.0.0.1', 9000)) # data, server_addr = client.recvfrom(1024) data = client.recvfrom(1024) print(data) client.close()
七、沾包问题
1、什么是沾包
网络通信中的"粘包"(Packet Sticking)是指发送方在发送数据时,连续发送的多个数据包被接收方接收时粘在一起,形成一个大的数据包。
这种情况可能会导致接收方无法正确解析和处理数据,从而引发数据解析错误。
2、沾包的原因
主要原因是网络传输的数据没有明确的边界,导致接收方无法准确分割接收到的数据。常见的导致粘包的情况包括:
- 发送方连续发送多个数据包时,这些数据包在网络传输过程中被合并为一个大的数据包。
- 接收方缓冲区大小较小,无法及时接收完整的数据包,导致多个数据包被合并在一起。如 client.recv(1024)
- 网络传输中存在延迟、拥塞等因素,导致数据包的发送和接收不同步。
3、解决粘包问题的方法有多种,其中常用的方法包括:
- 增加边界标识:发送方在数据包之间添加特定的边界标识符,接收方根据标识符来分割接收到的数据。例如,在每个数据包之后添加换行符或特定字符作为边界标识。
- 使用固定长度的数据包:发送方将数据按照固定长度进行分割并发送,接收方根据固定长度来解析数据包。这种方法需要发送方和接收方事先约定好固定长度。
- 使用长度字段:发送方在每个数据包的开头添加一个表示数据长度的字段,接收方先读取长度字段,然后根据长度读取相应长度的数据。这种方法需要发送方和接收方都严格按照长度字段进行处理。
4、struct 模块
struct 是 Python 中的一个内置模块,用于处理二进制数据和 Python 数据类型之间的相互转换。
它提供了一组函数,可用于在不同的数据类型之间进行打包(pack)和解包(unpack)操作,使得处理二进制数据更加方便和灵活
struct
模块的主要目的是处理不同机器之间的数据交换,或者处理与底层操作系统相关的二进制数据。它能够将Python数据类型和C结构体类型相互转换,使得数据在不同平台和编程语言之间能够正确地传递和解析。
struct
模块的常用函数包括:
pack(format, v1, v2, ...)
:将数据按照指定的格式(format)打包为字节序列。format
参数指定了数据的布局和类型,v1, v2, ...
参数表示要打包的数据。返回一个包含打包数据的字节序列。unpack(format, buffer)
:从字节序列中按照指定的格式(format)解包数据。format
参数指定了数据的布局和类型,buffer
参数为包含要解包的字节序列。返回一个包含解包数据的元组。calcsize(format)
:计算指定格式(format)的字节序列的长度。返回一个整数,表示字节序列的长度。
struct
模块支持的格式字符串中包含了多种数据类型和格式指示符,例如:
- 数字类型:
b
(有符号字节),B
(无符号字节),h
(有符号短整数),H
(无符号短整数),i
(有符号整数),I
(无符号整数),l
(有符号长整数),L
(无符号长整数),f
(单精度浮点数),d
(双精度浮点数)等。 - 字节顺序:
<
(小端序,低字节在前),>
(大端序,高字节在前),!
(网络字节顺序)等。 - 其他格式指示符:
s
(字节序列),x
(字节填充)等。
案例:
服务端
import socket import json import struct server = socket.socket() # 实例化server server.bind(('127.0.0.1', 8083)) # 绑定ip+端口 server.listen(5) # 开启监听 while True: sock, address = server.accept() # 接受用户发的数据 # 先接收客户端固定长度为4的字典报头数据 recv_first = sock.recv(4) # 第一次接收来自客户端的消息 print(recv_first) # 解析字典报头 dict_length = struct.unpack('i', recv_first)[0] # 接收字典数据 real_data = sock.recv(dict_length) # 第二次接收来自客户端序列化后的字典 print(real_data) # 解析字典(json格式的bytes数据 loads方法会自动先解码 后反序列化) real_dict = json.loads(real_data) print(real_dict) # 获取字典中的各项数据 data_length = real_dict.get('size') file_name = real_dict.get("file_name") recv_size = 0 with open('test.mp4', 'wb') as f: while recv_size < data_length: data = sock.recv(1024) # 第三次接收来自客户端的数据 recv_size += len(data) f.write(data)
注:
1、os.path.getsize(xxx.mp4)
返回的是文件大小,单位为字节。这里第三次收客户端发来的二进制格式的视频文件,这里计算长度。逻辑是收完二进制文件,其len之后的长度等价于文件的大小
2、recv_size 相当与计数器
3、with open只打开了一次,没有多次写覆盖的操作,循环写,写完再关闭。
客户端
import json import socket import struct import os client = socket.socket() # 买手机 client.connect(('127.0.0.1', 8083)) # 拨号 while True: data_path = os.path.dirname(os.path.abspath(__file__)) # print(os.listdir(data_path)) # [文件名称1 文件名称2 ] movie_name_list = os.listdir(data_path) for i, j in enumerate(movie_name_list, 1): print(i, j) choice = input('请选择您想要上传的电影编号>>>:').strip() if choice.isdigit(): choice = int(choice) if choice in range(1, len(movie_name_list) + 1): # 获取文件名称 movie_name = movie_name_list[choice - 1] # 拼接文件绝对路径 movie_path = os.path.join(data_path, movie_name) # 1.定义一个字典数据 data_dict = { 'file_name': 'XXX老师合集.mp4', 'desc': '这是非常重要的数据', 'size': os.path.getsize(movie_path), # 获取文件的大小,字节数 'info': '下午挺困的,可以提神醒脑' } data_json = json.dumps(data_dict) # 序列化字典,序列化以后为字符串 # 2.制作字典报头 data_first = struct.pack('i', len(data_json)) # 压缩i模式,i 表示int类型 #### 客户端连续3次给服务端发送消息 #### # 3.发送字典报头 client.send(data_first) # 4.发送字典 client.send(data_json.encode('utf8')) # 以字节形式发送序列化后的字典 # 5.发送真实数据 with open(movie_path, 'rb') as f: for line in f: client.send(line)