26--网络编程:socket套接字编程
一 socket介绍
# 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.最后关闭连接,一次交互结束
四 socket模块函数用法
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)
4.1 服务端函数
s.bind() # 绑定(主机,端口号)到套接字
s.listen() # 开始监听端口访问
s.accept() # 被动接受TCP客户的连接,(阻塞式)等待连接的到来
4.2 客户端函数
s.connect() # 主动初始化向服务器连接
s.connect_ex() # connect()函数的扩展版本,出错时返回出错码, 而不是抛出异常
4.3 公共函数
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
4.4 面向锁的函数
s.setblocking() # 设置套接字的阻塞与非阻塞模式
s.settimeout() # 设置阻塞套接字操作的超时时间
s.gettimeout() # 得到阻塞套接字操作的超时时间
4.5 面向文件的函数
s.fileno() # 套接字的文件描述符
s.makefile() # 创建一个与该套接字相关的文件
五 socket通信案例
5.1 基于TCP的套接字
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
5.1.1 简单套接字通信
###### 服务端
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 对象的操作
5.1.2 添加链接与通信循环的通信
###### 服务端
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()
5.1.3 报错解决:端口占用
# 报错:
在重启服务端时可能会遇到: [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 时间
5.2 基于UDP的套接字
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协议 不出现粘包问题
因为传输时是数据报形式,每段数据是加上了一个报头,是有边界的,每次传送的数据都是完整的。
6.1 粘包问题介绍
两台电脑在进行收发数据时,其实不是直接将数据传输给对方
对于发送者: 执行 sendall/send 发送消息时,是将数据先发送至自己网卡的 写缓冲区
再由缓冲区将数据发送给到对方网卡的读缓冲区
对于接受者: 执行 recv 接收消息时,是从自己网卡的读缓冲区获取数据
所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。
# TCP出现粘包问题的原因:
1.tcp是流式协议,数据像水流一样黏在一起,没有任何边界区分
2.上一次的数据没有接收干净,有残留,就会下一次结果混淆在一起
# 解决核心法门就是:每次都收干净,不要任何残留
6.2 粘包解决
6.2.1 struct 模块
struct模块 # 该模块可以把一个类型,如数字,转成固定长度的bytes
struct.pack('i',1111111111111) # 4位的bytes
6.2.2 固定模板
# 解决粘包问题最终版思路:(固定模板) 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='')
6.3 粘包案例
-
服务端
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实现并发
# socketserver模块中分两大类
server类 # 解决链接问题
request类 # 解决通信问题
# 并发:
IO密集 多线程
计算密集 多进程
7.1 基于TCP实现并发
服务端
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'))
7.2 基于UDP实现并发
服务端
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()