# 46. socket模块 - 网络套接字模块
# 网络基础
这一篇,不是整个网络编程都需要网络知识
网络基础我就不记了,记录个网址,讲的就是网络编程,里面就有网络知识
地址:https://www.cnblogs.com/LY-C/p/9089331.html
# socket模块
# socket模块概念
socket套接字分成二种
- 基于文件类型的套接字:AF_UNIX
- 基于unix系统开发出来,继承unix系统的特性,一切皆为文件,所以基于文件类型的套接字就是调用底层的文件系统来取数据的,
- 两个套接字进程运行在同一个机器,可以通过访问同一个文件系统来间接的完全通信
- 目前不使用
- 基于网络类型的套接字:AF_INET
- 还有其他的地址套接被爆了,但是AF_INET套接字使用最为广泛的一个
- 在Python中支持很多地址套接字
- 还有一个AF_INET6,是用于IPV6的,目前无使用,还是要知道为好
- 目前就使用基于网络类型的套接字:AF_INET
# 网络传输协议
网络传输协议分为二种
- TCP协议
- TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序
- UDP协议
- UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)
# socket模块初试
socket.socket() ## 是有几个选项,接下来看一下常用选项
def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None): ## 这是socket这个类的__init__方法
可以看出来:
family:使用什么套接字,默认使用AF_INET,网络类型套接字
type:使用什么协议,默认使用SOCK_STREAM,TCP协议
TCP协议:SOCK_STREAM
UDP协议:SOCK_DGRAM
proto:协议号通常为零,可以省略,或者在地址套接字为AF_CAN的情况下,协议应为CAN_RAW或CAN_RCM之一
fileno:如果指定了fileno,则其他参数将被忽略,使用带有指定文件描述符的套接字返回
和socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的
这可能有助于使用socket.close()关闭一个独立的插座
# socket模块的TCP用法
特点
- 发送时,需要进入连接服务器测试
- 一发一收
- 无法同时多台客户端聊天,只有等一台客户端停止断开连接,才能跟下一台客户端连接
# 编写能接收客户端的消息
## 服务端的代码 server.py
import socket
sk = socket.socket() ## 实例化一个对象,类来自socket模块中
sk.bind(("192.168.0.90",18080)) ## 绑定IP地址跟端口,到这个程序中,必须是以元组类型
sk.listen() ## 开机并监控
conn,addr = sk.accept() ## 接收客户的连接
ret = conn.recv(1024) ## 接收客户端发来的消息,1024代表的是:接收不超过1024字节的数据
print(ret.decode()) ## 打印客户端发来的信息
conn.close() ## 关闭链接
sk.close() ##关闭socket服务
## 客户端的代码 client.py
import socket
sk = socket.socket() ## 实例化一个对象,类来自socket模块中
sk.connect(("192.168.0.90",18080)) ## 尝试连接服务器,服务器IP跟端口 必须是以元组类型
sk.send("你好".encode("utf-8")) ## 发送消息,必须要用bytes类型的数据才能发送
sk.close() ## 关闭
执行顺序
先把服务端启动
在启动客户端
执行结果:
你好
# 简单完善"编写能接收客户端的消息"
## 服务端的代码 server.py
import socket
while 1:
print("本程序支持所有场景输入 Q 退出")
sk = socket.socket() ## 实例化一个对象,类来自socket模块中
sk.bind(("192.168.0.90",18080)) ## 绑定IP地址跟端口,到这个程序中,必须是以元组类型
sk.listen() ## 开机并监控
conn,addr = sk.accept() ## 接收客户的连接
ret = conn.recv(1024) ## 接收客户端发来的消息,1024代表的是:接收不超过1024字节的数据
print("==========客户端发来的消息==========")
print(ret.decode()) ## 打印客户端发来的信息
print("====================================")
user = input("请输入你要回复的消息:")
if user.upper() == "Q":
exit()
conn.send(user.encode("utf-8")) ## 回复客户端消息
conn.close() ## 关闭链接
sk.close() ##关闭socket服务
## 客户端的代码 client.py
import socket
while 1:
print("本程序支持所有场景输入 Q 退出")
sk = socket.socket() ## 实例化一个对象,类来自socket模块中
sk.connect(("192.168.0.90",18080)) ## 尝试连接服务器,服务器IP跟端口 必须是以元组类型
user = input("请输入你要发送的消息:")
if user.upper() == "Q":
exit()
sk.send(user.encode("utf-8")) ## 发送消息,必须要用bytes类型的数据才能发送
ret = sk.recv(1024) ## 接收服务端发来的消息,1024代表的是:接收不超过1024字节的数据
print("==========服务端发来的消息==========")
print(ret.decode("utf-8"))
print("====================================")
sk.close() ## 关闭
# 相对完善"编写能接收客户端的消息"
## 服务端的代码 server.py
import socket
sk = socket.socket()
sk.bind(('192.168.43.226',8090))
sk.listen()
while 1:
conn,addr = sk.accept()
while 1:
ret = conn.recv(1024)
res = ret.decode("utf-8")
if ret.decode("utf-8").upper() == "Q":
print("客户端退出")
break
print("==========客户端发来的消息==========")
print(ret.decode()) ## 打印客户端发来的信息
print("====================================")
user = input("请输入你要回复的消息:")
conn.send(user.encode("utf-8"))
if user.upper() == "Q":
print("主动退出")
break
conn.close()
sk.close()
## 客户端的代码 client.py
import socket
sk = socket.socket()
sk.connect(('192.168.43.226',8090))
while 1:
user = input("请输入你要发送的消息:")
sk.send(user.encode("utf-8"))
if user.upper() == "Q":
print("主动退出")
break
ret = sk.recv(1024)
print("==========服务端发来的消息==========")
print(ret.decode()) ## 打印客户端发来的信息
print("====================================")
if ret.decode("utf-8").upper() == "Q":
print("服务端退出")
break
sk.close()
## 比较完善接收客户端消息并能回复的功能,还可以客户端退出断开连接后还能接收到另个客户端的连接请求
# socket模块的UDP用法
特点:
- 发送时,无需进行服务端测试
- 多发多收
- 可以同时进行多台客户端聊天
- 可以遵守以下的规则
- 一发一收
- 只收
- 只发
- 可以遵守以下的规则
# 简单编写
## 服务端的代码 server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
sk.bind(('127.0.0.1',18080)) ## 创建绑定服务器地址
coon,addr = sk.recvfrom(1024) ## 接收括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
print(coon.decode("utf-8")) ## 输出接收到的消息
## 客户端的代码 client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
user = input("要发送的消息:") ## 要发送的消息
sk.sendto(user.encode("utf-8"),('127.0.0.1',18080)) ## 发送的消息,需要写二个参数,一个为要发送的数据,一个为要发送的客户端
## 简单实现了使用UDP协议的接收跟发送功能
# 简单完善编写
## 服务端的代码 server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
sk.bind(('127.0.0.1',18080)) ## 创建绑定服务器地址
while 1:
coon,addr = sk.recvfrom(1024) ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
print(coon.decode("utf-8")) ## 输出接收到的消息
user = input("请输入你要回复的信息:")
sk.sendto(user.encode('utf-8'),addr)
sk.close()
## 客户端的代码 client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
while 1:
user = input("要发送的消息:") ## 要发送的消息
sk.sendto(user.encode("utf-8"),('127.0.0.1',18080)) ## 发送的消息,需要写二个参数,一个为要发送的数据,一个为要发送的客户端
coon, addr = sk.recvfrom(1024) ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
print(coon.decode("utf-8")) ## 输出接收到的消息
## 实现了让客户端跟服务器进行聊天的功能,实现了服务端能对多个客户端进行聊天
# 比较完善编写 - 客户端发消息带名字
## 服务端的代码 server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
sk.bind(('127.0.0.1',18080)) ## 创建绑定服务器地址
while 1:
coon,addr = sk.recvfrom(1024) ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
print(coon.decode("utf-8")) ## 输出接收到的消息
user = input("请输入你要回复的信息:")
sk.sendto(user.encode('utf-8'),addr)
sk.close()
## 客户端的代码 client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
name = input("请输入你的用户名:")
while 1:
user = input("要发送的消息:") ## 要发送的消息
user = name + ":" + user
sk.sendto(user.encode("utf-8"),('127.0.0.1',18080)) ## 发送的消息,需要写二个参数,一个为要发送的数据,一个为要发送的客户端
coon, addr = sk.recvfrom(1024) ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
print(coon.decode("utf-8")) ## 输出接收到的消息
# 接收的消息带上颜色
## 服务端的代码 server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) ## 实例化socker类,并把网络协议更改为UDP协议
sk.bind(('127.0.0.1',18080)) ## 创建绑定服务器地址
dic = {'江凡':'\033[32m',"李城":"\033[33m"}
while 1:
coon,addr = sk.recvfrom(1024) ## 接收,括号中的是接收多少字节下的数据,coon=接收的数据,addr=客户端的IP跟端口
msg_r = coon.decode('utf-8')
name = msg_r.split(':')[0].strip()
color = dic.get(name, '') # get(key,default) 获取字典中key对应的value,如果没有key则返回default
print('%socket %socket \033[0m' %(color,msg_r))
user = input("请输入你要回复的信息:")
sk.sendto(user.encode('utf-8'),addr)
sk.close()
## 客户端的代码 client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)# udp协议
name = input('请输入您的名字:')
# 收发
while 1:
msg_s = input(('>>>'))
info = name + ' : ' + msg_s
sk.sendto(info.encode('utf-8'), ('127.0.0.1',8090)) # 发给谁的消息
msg_r,addr = sk.recvfrom(1024)# 接收来自于哪里的消息
print(msg_r.decode('utf-8'))
sk.close()
# 减少socket使用时候的操作
需要自定义一个模块文件,如下
## 自定义模块,my_socket
import socket
class My_socket(socket.socket):
def __init__(self,encoding="utf-8"):
self.encoding = encoding
super(My_socket,self).__init__(type=socket.SOCK_DGRAM)
def se(self,coon,addr):
return self.sendto(coon.encode(self.encoding),addr)
def re(self,num):
conn, addr = self.recvfrom(num)
return conn.decode(self.encoding),addr
## 服务端的代码 server.py
import my_socket
sk = my_socket.My_socket()
sk.bind(('127.0.0.1',18080))
dic = {'江凡':'\033[32m',"李城":"\033[33m"}
while 1:
coon,addr = sk.re(1024)
print(coon)
user = input("请输入你要回复的信息:")
sk.se(user,addr)
sk.close()
## 客户端的代码 client.py
import my_socket
sk = my_socket.My_socket()
name = input("请输入你的用户名:")
while 1:
user = input("要发送的消息:")
user = name + ":" + user
sk.se(user,('127.0.0.1',18080))
coon, addr = sk.re(1024)
print(coon)
# socket的其他用法
# 服务端套接字函数
方法 | 用法 |
---|---|
socket.bind() | 绑定(主机,端口号)到套接字 |
socket.listen() | 开始TCP监听 |
socket.accept() | 被动接受TCP客户的连接,(阻塞式)等待连接的到来 |
# 客户端套接字函数
方法 | 用法 |
---|---|
socket.connect() | 主动初始化TCP服务器连接 |
socket.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
# 公共用途的套接字函数
方法 | 用法 |
---|---|
socket.recv() | 接收TCP数据 |
socket.send() | 发送TCP数据 |
socket.sendall() | 发送TCP数据 |
socket.recvfrom() | 接收UDP数据 |
socket.sendto() | 发送UDP数据 |
socket.getpeername() | 连接到当前套接字的远端的地址 |
socket.getsockname() | 当前套接字的地址 |
socket.getsockopt() | 返回指定套接字的参数 |
socket.setsockopt() | 设置指定套接字的参数 |
socket.close() | 关闭套接字 |
# 面向锁的套接字方法
方法 | 用法 |
---|---|
socket.setblocking() | 设置套接字的阻塞与非阻塞模式 |
socket.settimeout() | 设置阻塞套接字操作的超时时间 |
socket.gettimeout() | 得到阻塞套接字操作的超时时间 |
# 面向文件的套接字的函数
方法 | 用法 |
---|---|
socket.fileno() | 套接字的文件描述符 |
socket.makefile() | 创建一个与该套接字相关的文件 |
socket.connect() | 主动初始化TCP服务器连接 |
socket.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
# 公共用途的套接字函数
方法 | 用法 |
---|---|
socket.recv() | 接收TCP数据 |
socket.send() | 发送TCP数据 |
socket.sendall() | 发送TCP数据 |
socket.recvfrom() | 接收UDP数据 |
socket.sendto() | 发送UDP数据 |
socket.getpeername() | 连接到当前套接字的远端的地址 |
socket.getsockname() | 当前套接字的地址 |
socket.getsockopt() | 返回指定套接字的参数 |
socket.setsockopt() | 设置指定套接字的参数 |
socket.close() | 关闭套接字 |
# 面向锁的套接字方法
方法 | 用法 |
---|---|
socket.setblocking() | 设置套接字的阻塞与非阻塞模式 |
socket.settimeout() | 设置阻塞套接字操作的超时时间 |
socket.gettimeout() | 得到阻塞套接字操作的超时时间 |
# 面向文件的套接字的函数
方法 | 用法 |
---|---|
socket.fileno() | 套接字的文件描述符 |
socket.makefile() | 创建一个与该套接字相关的文件 |
# socketserver模块 - 解决TCP无法多人连接
解决TCP协议无法多人连接的模块,也是基于socket模块的基础上
# socketserver模块的固定格式
import socketserver
class Msocket(socketserver.BaseRequestHandler): # 除了类名能修改之类,父类不能改
def handle(self): # 方法名不能修改,但是方法下的逻辑可以自由修改
# 收发的逻辑代码
pass
server = socketserver.TCPServer(('127.0.0.1',8080),MySocket) # 括号中的都可以修改,服务器的IP跟端口,还有创建的类的类名
server.serve_forever() # 代表开启一个永久性的服务
# socketserver模块的简单使用
# 服务端的代码
import socketserver
class Msocket(socketserver.BaseRequestHandler): # 除了类名能修改之类,父类不能改
def handle(self): # 方法名不能修改,但是方法下的逻辑可以自由修改
# 收发的逻辑代码
msg = self.request.recv(1024).decode("utf-8")
print(msg)
server = socketserver.TCPServer(('127.0.0.1',8080),Msocket) # 括号中的都可以修改,服务器的IP跟端口,还有创建的类的类名
server.serve_forever() # 代表开启一个永久性的服务
# 客户端的代码
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
msg_s = input('>>>')
sk.send(msg_s.encode('utf-8'))
print(sk.recv(1024).decode('utf-8'))
sk.close()
说实话,我暂时也不知道这个模块有什么用,他只能让多人连接,但是不能让多少进行通信
这模块差不多就像是在TCP中创建一个对列,让连接的用户进行这个对列,程序处理的时候调用这个对列
就算使用了这个模块,TCP也只能进行一对一通信,只是连接可以连接到而已
感觉使用这个模块让TCP拥有UDP的一些特性功能
# 网络传输问题之TCP-粘包
什么是粘包,发送端发送数据,接收端不知道应该如何去接收,造成的一种数据混乱的现象
**注意:**粘包只会在使用TCP协议传输才会发生,使用UDP则不会发生
socker数据传输过程
- 发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。
- 也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
- 而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
- 怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
- 例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
- 此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法 (opens new window)把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
TCP跟UDP发送数据长度的限制
- 用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送)
- 用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送
# 粘包发生的二种情况
在TCP协议传输中发生粘包的二种情况
- TCP协议的拆包机制
- 当发送端缓冲区的长度大于网卡的MTU时,TCP会将这次发送的数据拆成几个数据包发送出去。
- MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。
- MTU的单位是字节。 大部分网络设备的MTU都是1500。
- 如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。
- 当发送端缓冲区的长度大于网卡的MTU时,TCP会将这次发送的数据拆成几个数据包发送出去。
- 面向流的通信特点和Nagle算法
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
- 收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
- 这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
- 可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
← 介绍 47. subprocess模块 →