三、Socket简介
三、Socket简介
所有需要网络进行交互通信,都避免不了进行切切实实的数据传输过程,而这个过程在网络上是十分复杂的
我们作为开发人员,在需要进行开发借助互联网的软件时,不可避免的需要了解到网络中数据的传输过程
为了减少复杂的数据传输过程对开发的影响,出现了Socket模块,他将传输层及以下的数据传输过程给封装成一个模块,我们只需要调用调用该模块中的对应接口,即可实现复杂的网络通信

1、socket
socket简称套接字,是进程间通信的一种方式,它与其他进程间通信的主要不同是:他能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于socket来完成通信的,例如我们的浏览网页、qq聊天、收发email
-
不同电脑之间的通信需要使用socket,具体步骤
- 第一步:创建socket,并且连接
- 第二步:发送数据
- 第三步:关闭socket
'''
套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。例如:如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)
'''
2、socket 模块用法
2.1 创建socket模块
在python中使用socket模块的函数socket就可以完成
socket可以在不同的电脑间通信;还可以在同一个电脑的不同进程之间通信
socket属于Python的内置模块,无需下载
- 语法:
import socket
socket.socket(AddressFamily,Type)
'''
说明!
函数socket.socket创建一个socket,该函数带有两个参数:
1.AddressFamily:套接字家族
socket.AF_INET:用于Internet进程间通信,表示这个socket是用来进行网络连接,默认值
socket.AF_UNIX:用于同一台机器进程间通信
实际工作中常用AF_INET
2.Type:套接字类型
socket.SOCK_STREAM:流式套接字,主要用于TCP协议,默认值
socket.SOCK_DGRAM:数据包套接字,主要用于UDP协议
对于UDP连接,无需事先建立关系即可发送数据
socket.socket()括号内不加参数默认就是TCP链接
'''
2.2 服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字 s.listen() 开始TCP监听 s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
2.2.1 bind()
作用:服务器用来绑定自身的端口和IP,方便客户端的访问,如果不绑定,客户端则无法寻找到服务器,从而无法建立连接
- 语法:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.bind(address)
'''
address:本地主机的IP地址,参数是一个元组类型的数据,元组中有两个元素(n,m)
n:表示本地主机的IP地址,需要使用引号封装,例如:'192.168.1.1'
m:表示本地主机中程序的端口号,无需使用引号分装,例如:900
'''
在完成IP+PORT绑定操作后,即可使用accept方法来接收数据
2.2.1.1 绑定端口的意义
- 如果不绑定端口,那么resvfrom就无法接收对端发给本机的数据
- 如果resvfrom语句前存在sendto语句,那么resvfrom默认会接受sendto语句参数中目的IP设备的数据
- resvfrom语句在书写时,最好在该语句前加上bind方法,绑定一个端口作为数据接收的入口。
2.2.1.2 本地IP地址的选择
'''
对于本机,可以使用:
localhost
127.0.0.1
0.0.0.0
实际IP地址
以上三种IP都是代表本机的IP地址,都能够通过该IP来访问本地主机
当bind()方法内的IP地址参数为0.0.0.0
那么对于所有可以代表本地主机的IP地址,都可以访问到本机:(仅限本机)
127.0.0.1
实际IP地址
2.2.2 accept()
作用:服务器在与客户端建立连接后,使用该方法接收来自客户端的数据
- 语法:
import socket
#创建一个TCP的socket连接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.accept()
'''
accept()方法接收到的数据是一个元组,元组里有两个元素:(n,m)
n:是客户端的socket连接
m:是客户端的IP地址和端口号,元组数据类型
'''
2.2.2.1 accept回应数据
注意:
当TCP服务器使用accept方法获取客户端的数据时,可以通过accept数据元组的第一个元素来向客户端发送回应信息
'''
accept()获取的是一个元组,元组中有两个元素:
#第0个元素是客户端的socket连接
#第一个元素是客户端的IP地址和端口号
client_socket,client_addr = server_socket.accept()
使用accept数据元组的第一个元素来向客户端发送回应信息
client_socket.send(('hello',encode('utf8')))
'''
2.2.3 listen()
作用:服务器一般与客户端的关系是一对多,对于数量过多的客户端同时登录时,需要设置listen()方法来设置缓冲区,防止服务器崩溃
- 语法:
import socket
#创建一个TCP的socket连接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#将本服务器作为被动端,被动等待客户端发起TCP连接
s.listen(int)
'''
int:代表填入一个int类型的数据
如果填入值为128,代表允许同时与该服务器连接的客户端超出部分的缓冲区长度,表示可以多容纳128个客户端
'''
2.3 客户端套接字函数
s.connect() 主动初始化TCP服务器连接 s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
2.3.1 connect()
作用:用于TCP客户端向TCP服务器发起TCP连接,其中参数输入TCP服务器的IP地址与端口号
- 语法:
import socket
#创建基于TCP协议的socket链接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(address)
'''
address:目的服务器的IP地址和端口号,参数是一个元组类型的数据,元组中有两个元素(n,m)
n:表示目标的IP地址,需要使用引号封装,例如:'192.168.1.1'
m:表示目标主机中程序的端口号,无需使用引号分装,例如:900
'''
2.4 公共用途的套接字函数
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() 关闭套接字
2.4.1 send()
作用:发送数据,需要前置条件:connect()指定目的服务器的IP和端口号
- 语法:
import socket
#创建基于TCP协议的socket链接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect('IP地址',端口号)
s.send(data)
'''
data:要发送的数据,它是二进制的数据
'''
2.4.2 sendto()
作用:向指定的IP地址和端口号发送数据,使用sendto()方法时,无需使用connect来指定服务器的IP地址和端口号
- 语法:
import socket
a = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
a.sendto(data,address)
'''
data:要发送的数据,它是二进制的数据
address:数据的目的接收主机的IP地址,参数是一个元组类型的数据,元组中有两个元素(n,m)
n:表示目标的IP地址,需要使用引号封装,例如:'192.168.1.1'
m:表示目标主机中程序的端口号,无需使用引号分装,例如:900
如果socket创建的是一个UDP连接,无需事先建立关系即可发送数据
如果socket创建的是一个TCP连接,需要在发送数据之前和对端建立连接
'''
s.sendto('hello'.encode('utf8'),('192.168.1.1',900))
2.4.3 recvfrom()
作用:接收对端设备数据
注意:
recvfrom方法是一个阻塞式的命令,如果recvfrom没有接收到对方发来的数据,recvfrom就会一直霸占系统的资源使其他命令无法继续运行
- 语法:
import socket
#1.创建socket UDP连接,用于本电脑内不同进程间的数据传递
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#2.使用bind方法绑定IP和端口号,实现数据接收,注意bind参数为元组,所以需要两个括号(在使用recvfrom方法接收数据前需要绑定自身的IP和端口号)
s.bind(('192.168.1.102',9000))
#3.使用recvfrom方法接收数据
content = s.recvfrom(1024)
#recvfrom必须填写一个int类型的数值,1024表示每次获取数据的长度的限制为1024字节
#如果一个文件超过1024字节,那么就无法一次将其读取完毕,需要使用循环语句来获取所有信息
'''
使用recvfrom方法接收到的数据是一个元组类型的数据,元组中有两个元素:
(n,m)
n:是接收到的数据
m:是发送方的IP地址和端口号,以元组的形式嵌套
'''
#5.关闭socket连接
s.close()
对于数据接收之前,我们需要执行一个前置操作(绑定IP和端口号),才能够接收到数据
2.4.4 recv()
recv方法是一个阻塞式的命令,如果recvfrom没有接收到对方发来的数据,recvfrom就会一直霸占系统的资源使其他命令无法继续运行
2.4.4.1 获取客户端发来的数据
- 语法:
import socket
#创建一个TCP的socket连接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client_socket,client_addr = s.accept()
#UDP中使用recvfrom获取数据,在TCP中需要使用rev获取数据
#需要使用客户端的socket来调用recv方法获取发送给服务器的内容,recv获取的都是二进制需要进行转换
client_socket.recv(1024).decode('utf8')
#1024表示每次获取数据的长度的限制为1024字节
#如果一个文件超过1024字节,那么就无法一次将其读取完毕,需要使用循环语句来获取所有信息
2.4.4.2 获取服务器发来的数据
- 语法:
import socket
#创建一个TCP的socket连接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('服务器IP地址',服务器端口号))
s.send('发送信息'.encode('utf8'))
-------------------------------------
#以上均是作为一个客户端的标准语法
#以下接收数据语法要求服务器的语法为:
'''
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('本地IP',本地端口))
s.listen(128)
a,b = s.accept()
---------------------------------------
以上均是作为一个服务器的标准语法
以下语法可以实现客户端接收到服务器的信息
a.send('发送给客户端数据'.encode('utf8'))
s.close()
'''
a = s.recv(1024).decode('utf8')
print(a)
s.close()
2.4.5 close()
作用:关闭一个连接或文件
- 语法:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.close()
2.5 面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式 s.settimeout() 设置阻塞套接字操作的超时时间 s.gettimeout() 得到阻塞套接字操作的超时时间
2.6 面向文件的套接字函数
s.fileno() 套接字的文件描述符 s.makefile() 创建一个与该套接字相关的文件
3、TCP客户端和服务器的工作流程

3.1 TCP客户端标准流程
import socket
#1.创建一个TCP socket链接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#2.指定对端IP地址和端口号
s.connect(('IP地址',端口号))
#3.向建立连接的对端发送数据,数据要求为二进制
s.send(data)
#4.接收来自服务器的数据
server_data = s.recv(1024).decode('utf8')
#5.断开socket连接
s.close()
3.2 TCP服务器标准流程
import socket
#1.创建一个TCP socket链接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#2.绑定本地IP和端口,方便客户端访问
s.bind(('IP地址',端口))
#3.设置客户端访问的缓冲区
s.listen(1024)
#4.接收客户端连接
a,b = s.accept()
#5.获取客户发来的数据,并进行解码
client_data = a.recv(1024).decode('utf8')
#6.向客户端发送数据,数据需要是二进制
a.send(data)
#7.关闭与该客户端的TCP连接(必选)
a.close()
#8.关闭socket连接
s.close()
3.3 TCP socket遇到的问题
3.3.1 服务端重启发生地址占用问题

这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法)
3.3.1.1 解决方法1:
#加入一条socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
'''
setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
作用:重用IP和端口,需要在bind()代码之前设置
setsockopt:
set:设置
sock:sock连接
opt:option缩写,即可选项
'''
phone.bind(('127.0.0.1',8080))
3.3.1.2 解决办法2:
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
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
然后执行 /sbin/sysctl -p 让参数生效。
'''
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
'''
3.3.2 TCP粘包问题
须知:只有TCP有粘包现象,UDP永远不会粘包
socket收发消息原理图:

1.TCP收发数据原理:
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
2.UDP收发数据原理:
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
3.3.2.1 接收方引起的粘包
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
3.2.2.1.1 服务端
from socket import *
ip_port=('127.0.0.1',8080)
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addr=tcp_socket_server.accept()
data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()
3.3.2.1.2 客户端
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
s.send('hello feng'.encode('utf-8'))
3.3.2.2 发送方引起的粘包
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
3.3.2.2.1 服务端
from socket import *
ip_port=('127.0.0.1',8080)
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addr=tcp_socket_server.accept()
data1=conn.recv(10)
data2=conn.recv(10)
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()
3.3.3.2.2 客户端
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
3.3.2.3 粘包问题解决
粘包问题的根本原因就是无法判断拿取数据时的起始和终止,导致在从缓存中拿取数据时,少拿了数据,从而影响下次拿取数据时,带上了上次未拿完的数据,这就是粘包
解决的核心法门就是收干净,每次都将执行结果给收干净
解决粘包的理论步骤: 1. 获取对端发送数据的具体长度
- 使用循环,不停使用recv从缓存中获取数据,直到获取的总数据长度=对端发送数据的长度
3.3.2.3.1 自定义协议解决粘包
上述已经讲过如何解决粘包的原理,但是实际操作中,我们无法单独将数据长度发送过去(TCP会将短数据粘包),需要进行一些复杂操作,单独将数据长度发送过去
原理如下:
1. 先发头信息(固定长度的二进制):对数据的描述
2. 再发真实的数据
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct
struct.pack('模式',data)
'''
data:表示想要转换成固定长度的数据
struct.pack是将我们填入的数据转换成struct类型的数据,这种数据需要使用struct中的方法来解码
'''
struct.unpack('模式',data)
'''
data:表示想要解码的struct数据
struct.unpack就是将struct.pack转换后的数据给还原,其中模式需要和struct.pack一致,返回的数据是一个元组类型
'''
模式如下

3.3.2.3.2 解决粘包问题的终极方法
以上仅仅只是使用简单的传输长度,从而实现客户端确认数据的长度,来将数据取干净
对于上述操作我们还可以进行更精细化的操作
具体步骤不变:
-
制作一个报文头,头中包含:
- 描述传输给客户端的数据
- 该数据的长度
- 数据的sha256加密后的编码
-
首先将该报文的长度发送给客户端
-
客户端接收到报文头后,根据报文头的数据长度来接收数据
3.3.3 客户端中断导致TCP连接未断开问题
在TCP连接下,客户端与服务器建立连接后,如果客户端突然断电,从而没有向服务端发送TCP断开连接请求,就会导致TCP服务器一直认为对端在线,从而引发后续问题:
-
在unix/windwos系统中,TCP服务端中的recv方法会认为TCP客户端发送了数据,从而读取缓存,获取一个空
- 在unix下,一旦系统获取到空,那么意味着这是一种异常行为,客户端非法断开了连接
3.4 链接循环
TCP服务器使用accept()来与TCP客户端来进行TCP链接,因为TCP链接的特殊性,目前我们只能服务端与客户端一一对应,当客户端下线时,我们可以重新调用accept()方法,来与新的客户端进行交互
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
while True:
a,b = s.accept
3.5 半链接池
listen()方法可以设置TCP客户端的连接请求的缓存区,也可以称之为半链接池
对于TCP来说,没有其他手段的情况下,同一时间只能与一个客户端进行交互通信,对于其他客户端的请求都无法响应
在Python中,使用listen()方法来保存其他客户端的TCP请求,一旦服务器正在链接的客户端断开,就会从listen()中顺序取出一个TCP客户端请求,并与之建立链接,整个过程十分迅速
listen()创建的半连接池的空间也是有限的,一旦客户端请求的链接超出了listen()设置的空间大小,那么后续的客户端TCP请求都会被拒绝
4、UDP客户端和服务器的工作流程
4.1 UDP客户端
import socket
#1.创建一个UDP socket连接
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#2.指定服务器的IP地址和端口号
s.connect(('192.168.1.2',8000))
#3.填入想要发送的数据
s.send('hello'.encode('utf8'))
#4.接收来自UDP服务器发来的数据
a = s.recv(1024).decode('utf8')
print(a)
#5.关闭socket连接
s.close()
4.2 UDP服务器
import socket
#1.创建一个UDP socket连接
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#2.绑定本地IP地址和端口号,方便客户端访问
s.bind(('192.168.1.2',8000))
#3.使用recvfrom接收客户端的数据和IP+PORT信息
a,b = s.recvfrom(1024)
print(a.decode('utf8'))
#4.使用sendto方法向客户端发送数据
s.sendto('hello'.encode('utf8'),b)
#5.关闭socket连接
s.close()
4.3 UDP与TCP的不同
4.3.1 粘包问题
UDP是基于数据包来接收数据的,它并不存在数据的先后,每一个UDP数据包都可以看成一个独立的通信,所以UDP不会出现粘包问题,但是容易受网络影响,出现丢包
UDP对于单个数据包的数据,如果拿取不干净,则该包剩下的数据直接丢弃
recvfrom()
如果UDP客户端和服务器使用recvfrom()来获取数据,那么它会从自身的缓存中拿出数据+(IP+Port)的组合
对于这种情况,哪怕客户端或者服务器发出空数据,对端在取数据时也不会报错,因为数据为空,但是可以拿出IP+Port
recv()
使用recv()从内存中拿数据时是不会将IP+Port一同拿出的,就会导致当对端发送一个空数据时,会出现recv()拿不到数据,从而阻塞
4.3.2 半链接池
对于UDP来说,他与对端从未建立连接,只是一味的接收、发送,对于数据的发送者是谁并不关心,这也是UDP不安全的原因之一
UDP不存在半连接池的概念
以这种情况来考虑,对于多个客户端访问服务器时,无需设置半连接池,可以有多个客户端向服务器发送数据,然后服务器根据缓存中数据的排序,顺序取出数据
还可以使用recvfrom,确定数据的源端,从而向该源端回应信息
5、socketserver实现并发
基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环
socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)
5.1 server类

5.2 request类

5.3 继承关系



5.4 实现TCP并发
服务器
import socketserver
'''
服务端需要实现两个功能:
1. 链接循环:
服务器循环从半连接池中取出链接请求与对端建立双向链接,拿到链接对象
2. 通信循环:
服务器与链接端实现循环通信,能够实现交互式数据通信
'''
#继承socketserver模块中BaseRequestHandler类的功能
class MyRequestHandle(socketserver.BaseRequestHandler):
#必须重写 handle方法
#handle方法可以实现通信循环
def handle(self):
'''
self.request 如果是TCP协议,则代表 对端的链接对象,即:
a,b = s.accept() 中的 a , 客户端的socket链接对象
self.client_address 如果是TCP协议,则代表 对端的IP地址和端口号,即:
a,b = s.accept() 中的 b , 客户端的IP地址和端口号
'''
print(self.request)
print(self.client_address)
while True:
try:
msg = self.request.recv(1024).decode('utf8')
print(msg,len(msg))
if len(msg) == 0:
break
self.request.send(msg.upper().encode('utf8'))
except Exception:
break
self.request.close()
'''
socketserver.Threading... 创建一个线程
Threading就是代表线程的意思
ThreadingTCPServer(('IP地址',端口号),自己定义的类)
('IP地址',端口号):绑定本地服务器的IP地址和端口号
自己定义的类:
socketserver.Forking... 创建一个进程
Forking就是进程的意思
PS:在windows系统中,不支持Forking,因为在Forking造进程的方法就是调用os.fork接口,这个接口是针对unix系统的创建进程接口,对于windows需要使用其他接口,所以Forking无法在Windows中使用
'''
s = socketserver.ThreadingTCPServer(('127.0.0.1',8000),MyRequestHandle)
'''
serve_forever():永久提供服务,即链接循环
serve_forever()对应的就是以下操作,该函数源码也是大概如下:
while True:
a,b = s.accept()
serve_forever() 每建立一个链接都会创建一个线程,然后启动该线程,并将a,b参数封装到ThreadingTCPServer(('127.0.0.1',8000),MyRequestHandle)中指定的类中,而每个线程工作时,都会调用该类中的handle方法,所以可以使用handle方法实现通信循环
'''
s.serve_forever()
客户端
import socket
#1.创建一个TCP socket链接
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#2.指定对端IP地址和端口号
s.connect(('127.0.0.1',8000))
while True:
try:
c = input('>>>:')
if len(c) ==0 :
break
s.send(c.encode('utf8'))
server_data = s.recv(1024).decode('utf8')
print(server_data)
except Exception:
break
5.5 实现UDP并发
服务器
import socketserver
'''
socketserver对于UDP的并发操作与TCP原理基本一致,唯一不同的在于self.request获取的内容
'''
class MyRequestHandlde(socketserver.BaseRequestHandler):
def handle(self):
'''
self.request 获取的是一个元组数据(x,y):
x:UDP Client 发送给服务器的数据,数据为二进制
y:UDP Client 的socket链接对象,可以使用该链接对象对UCP Client发送数据
self.client_address 获取的是一个元组数据 (m,n):
m:UDP Client 的IP地址,数据类型为字符串
n:UDP Client 的端口号,数据类型为int
'''
print(self.request)
print(self.client_address)
self.request[1].sendto(self.request[0].upper(),self.client_address)
'''
因为UDP不存在建立链接,即链接循环,所以对于同一个UDP Client,在不同时间下对于UDP 服务器来说都是一个全新的链接,都会创建一个线程来进行通信,所以在handle中不用添加while循环来进行通信循环
如果基于UDP链接,在handle中添加了while循环,那么会出现CPU不停切换线程执行while循环中的代码,从而影响正常操作
'''
s = socketserver.ThreadingUDPServer(('127.0.0.1',8000),MyRequestHandlde)
s.serve_forever()
客户端
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.connect(('127.0.0.1',8000))
while True:
try:
c = input('>>>:')
s.send(c.encode('utf8'))
if len(c) == 0:
break
a = s.recv(1024).decode('utf8')
print(a)
except Exception:
break
s.close()

浙公网安备 33010602011771号