# Socket翻译为套接字 是应用层与TCP/IP协议族通信之间的抽象层 是一组接口,把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用 # 在设计模式中 Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面 对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
# unix一切皆文件 基于文件的套接字调用的就是底层的文件系统来取数据 两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
# AE_INET家族有很多地址家族 AF_INET是使用最广泛的一个 由于我们只关心网络编程,所以大部分时候只使用 AF_INET
# 服务器端 1.先初始化Socket 2.与端口绑定(bind) 3.对端口进行监听(listen) 4.调用accept阻塞,等待客户端连接 # 客户端 1.初始化一个Socket 2.连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了 # 传输数据 1.客户端发送数据请求 2.服务器端接收请求并处理请求,然后把回应数据发送给客户端 3.客户端读取数据 4.最后关闭连接,一次交互结束
import socket # 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 # 获取tcp/ip套接字 tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_sock = socket.socket() # 获取udp/ip套接字 udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind() # 绑定(主机,端口号)到套接字 s.listen() # 开始监听端口访问 s.accept() # 被动接受TCP客户的连接,(阻塞式)等待连接的到来
s.connect() # 主动初始化向服务器连接 s.connect_ex() # connect()函数的扩展版本,出错时返回出错码, 而不是抛出异常
s.recv() # 接收tcp数据 s.send() # 发送tcp数据 s.sendall() # 发送完整的TCP数据 (本质就是循环调用send) # 区别: send 在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完 sendall 在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) s.recvfrom() # 接收UDP数据 s.sendto() # 发送UDP数据 s.getpeername() # 连接到当前套接字的远端的地址 s.getsockname() # 当前套接字的地址 s.getsockopt() # 返回指定套接字的参数 s.setsockopt() # 设置指定套接字的参数 s.close() # 关闭套接字ss
s.setblocking() # 设置套接字的阻塞与非阻塞模式 s.settimeout() # 设置阻塞套接字操作的超时时间 s.gettimeout() # 得到阻塞套接字操作的超时时间
s.fileno() # 套接字的文件描述符 s.makefile() # 创建一个与该套接字相关的文件
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
###### 服务端 import socket # 1.买手机 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 流式协议=》 tcp协议 # 2.绑定手机卡 server.bind(('127.0.0.1', 8080)) # 参数是元祖形式 # 端口 0-65535,1024之前都被系统保留使用 # 3.开机 server.listen(5) # 5指的是半连接池的大小,可以直接 5 # 4.等待电话连接请求,拿到电话连接conn conn, client_addr = server.accept() # 会产生一个元祖,包含一个连接对象和客户端的IP端口地址 # 5.进行通话通信,收发消息 data = conn.recv(1024) # 一次接受的最大数据量为1024 Bytes,收到的是bytes类型 print('客户端发来的消息:', data.decode('utf-8')) conn.send(data.upper()) # 6.关闭电话连接(必选的回收资源的操作) conn.close() # 7. 关机(可选操作,通常不会关闭) 服务器关闭 server.close() # 有时候关闭后,端口还被占用,是因为这一步是操作系统去执行端口释放,可能会有延迟。 ###### 客户端 import socket # 1.买手机 client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # 2.拨通电话连接请求 client.connect(('127.0.0.1', 8080)) # 3.通信,收发消息 client.send('hello edmond hahaha'.encode('utf-8')) # 发送必须是bytes类型 # client.send(b'hello edmond hahaha') data = client.recv(1024) print(data.decode('utf-8')) # 4. 关闭连接(必选的回收资源的操作) client.close() # 注: 客户端全是由socket 对象 client来调用 服务端 有连接accept对象 和socket 对象的操作
###### 服务端 import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 流式协议=》 tcp协议 server.bind(('127.0.0.1', 8080)) server.listen(5) while True: # 链接循环 实际应该是多线程来链接 conn, client_addr = server.accept() while True: # 通信循环 # 针对Windows系统,客户端非法断开,会抛出异常,故采用异常处理方法,断开连接 try: data = conn.recv(1024) """ # 在Linux系统中,一旦data收到的是空,就意味着是一种异常的行为:客户端非法断开链接了 if len(data) == 0 : break """ if data.decode('utf-8') == 'quit': break print('客户端发来的消息:', data.decode('utf-8')) conn.send(data.upper()) except Exception: break conn.close() ###### 客户端 import socket client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) client.connect(('127.0.0.1', 8080)) while True: msg = input('请输入要发送的信息>>>:').strip() if len(msg) == 0 :continue # 注意 这里请求的quit 要先发送到服务器端,再两边分别判断断开 client.send(msg.encode('utf-8')) if msg == 'quit': break data = client.recv(1024) print(data.decode('utf-8')) client.close()
# 报错: 在重启服务端时可能会遇到: [Error 48] Address already in use # 原因: 由于你的服务端仍然存在四次挥手的time_wait状态,在占用端口地址 # 解决办法 # 方式1:在监听端口前,加入一条socket配置 重用ip和端口 server=socket(AF_INET,SOCK_STREAM) server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 重用ip和端口 server.bind(('127.0.0.1',8080)) # 方式2:通过调整linux内核参数解决 发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决 # 1.编辑文件,加入以下内容 vi /etc/sysctl.conf net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 # 2.执行命令 让参数生效 /sbin/sysctl -p # 参数解读: tcp_syncookies = 1 # 表示开启SYN Cookies 默认为0,表示关闭 当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击 tcp_tw_reuse = 1 # 表示开启重用 默认为0,表示关闭 允许将TIME-WAIT sockets重新用于新的TCP连接 tcp_tw_recycle = 1 # 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭 tcp_fin_timeout = 30 # 修改系統默认的 TIMEOUT 时间
udp是无链接的,先启动哪一端都不会报错
###### 服务端 # 注意: 1.udp协议 sendto 与 recvfrom 一定是 一一对应的,不然数据会丢失 2.虽然 先启动客户端与服务端 都没有问题,但是如果先启动客户端的话,发送数据到局域网, 因为没有服务端接受,数据就会被丢掉。所以,一般还是先启动服务端 3.udp协议 不出现粘包问题,因为传输时是数据报形式,每段数据是加上了一个报头,是有边界的,每次传送的数据都是完整的。 4.若接收端的缓冲池大小 小于 发送的数据大小时,接收端会出现 只接受到 部分数据,还有部分数据会丢失。 5. udp协议 通常 是传送小文件的,太大的话,不稳定,一般是512字节。 from socket import * server = socket(AF_INET, SOCK_DGRAM) # 数据报协议====》udp协议 server.bind(('127.0.0.1', 8080)) while True: ask_data, client_addr = server.recvfrom(1024) print('客户端说:', ask_data.decode('utf-8')) recv_data = input('服务端说:') server.sendto(recv_data.encode('utf-8'), client_addr) server.close() ###### 客户端 from socket import * client = socket(AF_INET, SOCK_DGRAM) while True: ask_data = input('客户端说:') client.sendto(ask_data.encode('utf-8'), ('127.0.0.1', 8080)) recv, server_addr = client.recvfrom(1024) # 是一个元祖,包含数据和收到的IP地址 print('服务器说:', recv.decode('utf-8')) client.close()
只有TCP有粘包现象,UDP永远不会粘包
# 粘包问题 主要因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。 # udp协议 不出现粘包问题 因为传输时是数据报形式,每段数据是加上了一个报头,是有边界的,每次传送的数据都是完整的。
两台电脑在进行收发数据时,其实不是直接将数据传输给对方 对于发送者: 执行 sendall/send 发送消息时,是将数据先发送至自己网卡的 写缓冲区 再由缓冲区将数据发送给到对方网卡的读缓冲区 对于接受者: 执行 recv 接收消息时,是从自己网卡的读缓冲区获取数据 所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。 # TCP出现粘包问题的原因: 1.tcp是流式协议,数据像水流一样黏在一起,没有任何边界区分 2.上一次的数据没有接收干净,有残留,就会下一次结果混淆在一起 # 解决核心法门就是:每次都收干净,不要任何残留
struct模块 # 该模块可以把一个类型,如数字,转成固定长度的bytes struct.pack('i',1111111111111) # 4位的bytes
# 解决粘包问题最终版思路:(固定模板) struct+json ###### 一、发送端总体思路: 先定义头;将头转成json字符串;再将头的长度打包;依次发送头的长度、头信息、真实数据 # 1.拿到需要发送数据的总大小 total_size = len(stderr_res)+len(stdout_res) # 2. 定义头 为字典,包含头的固定长度和其他描述信息,包括数据的总大小 total_size = xxxx等 header_dic = { 'filename': '远程命令的结果', 'total_size': 555, 'else_inf': '其他信息'} # 3.将字典头 转成json 字符串,并进行编码为可以发送的 二进制 字符串 json_str = json.dumps(header_dic) json_str_bytes = json_str.encode('utf-8') # 4.取到转化json 字符串后的头长度大小,并利用 struct 模块,将头的长度大小,打包成固定大小的Bytes 类型 x = struct.pack('i', len(json_str_bytes)) # 5.再将头的长度信息,发送过去 conn.send(x) # 6.再将头的信息,发送过去 conn.send(json_str_bytes) # 7.最后 发送真实数据信息 ###### 二、接收端总体思路 先接受到头,并把数据的总大小 total_size 解压出来 # 1.先接受头 (先收4个字节,从中提取接下来要收的头的长度) x = client.recv(4) # 2.利用 struct.unpack(),将头的长度 解压出来, head_len = struct.unpack('i', x)[0] # 解压出来是一个元祖:(x,) # 3.根据头的长度,将头信息由json 转成原python类型:字典,并打印头 json_str_bytes = client.recv(head_len) json_str = json_str_bytes.decode('utf-8') # 这两步可以放一起: json_str = client.recv(head_len).decode('utf-8') header_dic = json.loads(json_str) print(header_dic) # 4.把字典中 key为 total_size的值 取出来; total_size = header_dic.get('total_size') # 5.最后 根据total_size,循环接受真实的数据 recv_size = 0 while recv_size < total_size: recv_data = client.recv(1024) recv_size += len(recv_data) print(recv_data.decode('gbk'), end='')
服务端
import struct import subprocess import json from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8080)) server.listen(5) # 链接循环 while True: conn, client_addr = server.accept() # 通信循环 while True: try: cmd = conn.recv(1024) if len(cmd) == 0:break print('操作的命令:', cmd.decode('utf-8')) obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout_res = obj.stdout.read() stderr_res = obj.stderr.read() # 1.拿到需要发送数据的总大小 total_size = len(stderr_res)+len(stdout_res) # 2. 定义头 为字典,包含头的固定长度和其他描述信息,包括数据的总大小 total_size = xxxx等 head_dic = { 'filename': '远程命令的结果', 'total_size': total_size, 'else_inf': '其他信息' } # 3.将字典头 转成json 字符串,并进行编码为可以发送的 二进制 字符串 head = json.dumps(head_dic).encode('utf-8') # 4.取到转化json 字符串后的头长度大小,并利用 struct 模块,将头的长度大小,打包成固定大小的Bytes 类型 x = len(head) header = struct.pack('i', x) # 5.再将头的长度信息,发送过去 conn.send(header) # 6.再将头的信息,发送过去 conn.send(head) # 7.最后,发送真实数据信息 conn.send(stdout_res) conn.send(stderr_res) except Exception: break conn.close()
客户端
import struct import json from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8080)) # 通信循环 while True: cmd = input('请输入操作指令:').strip() if len(cmd) == 0: continue client.send(cmd.encode('utf-8')) # 1.先接受到头的长度 header = client.recv(4) # 2.利用 struct.unpack(),将头的长度 解压出来, x = struct.unpack('i', header)[0] # 解压出来是一个元祖:(x,) # 3.根据这个长度,将头信息由json 转成原python类型:字典 head = client.recv(x).decode('utf-8') head_dic = json.loads(head) # 测试打印下字典头 print(head_dic) # 4.把字典中 key为 total_size的值 取出来; total_size = head_dic.get('total_size') # 5.最后 根据total_size,循环接受真实的数据 recv_size = 0 while recv_size < total_size: recv_data = client.recv(1024) # 本次接受,最大接受为1024 Bytes recv_size += len(recv_data) print(recv_data.decode('gbk'), end='') print() client.close()
# socketserver模块中分两大类 server类 # 解决链接问题 request类 # 解决通信问题 # 并发: IO密集 多线程 计算密集 多进程
服务端
import socketserver # 基于tcp的socketserver我们自己定义的类中的 self.server # 套接字对象 self.request # 一个链接,tcp是 这个链接收发数据,而udp是没有链接,是套接字对象收发数据 self.client_address # 客户端地址 # 继承 BaseRequestHandler类 写通信逻辑 class MyRequestHandle(socketserver.BaseRequestHandler): def handle(self): print(self.request) # 如果是tcp协议,self.request====>conn连接对象 print(self.client_address) # self.client_address====>conn连接对象的IP和端口 while True: try: msg = self.request.recv(1024) if msg == 0:break print('客户端发来的消息:', msg.decode('utf-8')) self.request.send(msg.upper()) except Exception: break self.request.close() # 使用 ThreadingTCPServer类 开启多线程链接循环 server_obj = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyRequestHandle) server_obj.serve_forever() # 就等同于链接循环,并启动一个线程,把链接对象 conn 和 客户端地址信息 client.address 传递过去
客户端
import socket client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) conn = client.connect(('127.0.0.1', 8080)) while True: msg = input('>>>请输入:').strip() client.send(msg.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8'))
服务端
import socketserver import time # 基于udp的socketserver我们自己定义的类中的 self.request # 是一个元组 (客户端发来的数据,服务端的udp套接字对象) self.client_address # 即客户端地址 # 继承 BaseRequestHandler类 写通信逻辑 class MyRequestHandle(socketserver.BaseRequestHandler): def handle(self): client_data = self.request[0] server = self.request[1] print('客户端:{}发来的数据:{}'.format(self.client_address, client_data)) server.sendto(client_data.upper(), self.client_address) time.sleep(10) # 使用 ThreadingUDPServer类 开启多线程链接循环 server_obj = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyRequestHandle) server_obj.serve_forever()
客户端
from socket import * client = socket(AF_INET, SOCK_DGRAM) while True: msg = input('>>>请输入:').strip() if msg == 'q': break client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080)) recv, server_addr = client.recvfrom(1024) print(recv.decode('utf-8')) client.close()