socket
一、os七层
图1
TCP/IP协议族包括运输层、网络层、链路层。现在你知道TCP/IP与UDP的关系了吧。
socket层
在图1中,我们没有看到Socket的影子,那么它到底在哪里呢?还是用图来说话,一目了然。
socket是什么?
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
套接字发展及分类
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
socket()模块函数用法
import socket socket.socket(socket_family,socket_type,protocal=0) socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。 获取tcp/ip套接字 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 获取udp/ip套接字 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 由于 socket 模块中有太多的属性。我们在这里破例使用了‘from module import *‘语句。使用 ‘from socket import *‘,我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。 例如tcpSock = socket(AF_INET, SOCK_STREAM)
服务端套接字函数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() 创建一个与该套接字相关的文件socket通信流程与打电话流程类似,我们就以打电话为例来实现一个low版的套接字通信
# 简版 # socket服务端 import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#相当于买手机 phone.bind(("127.0.0.1",8080))#插卡,必须是元组的形式 phone.listen(5)#开机5代表等待接听的个数 coon,addr = phone.accept()#接电话 print("tcp的连接",coon)#coon 就是tcp三次握手的建立的连接 # 打印的信息: # tcp的连接 <socket.socket fd=232, family=AddressFamily.AF_INET, # type=SocketKind.SOCK_STREAM, proto=0, laddr=(‘127.0.0.1‘, 8080), raddr=(‘127.0.0.1‘, 5280)> print("客户端的地址",addr) # 打印的信息: # 客户端的地址 (‘127.0.0.1‘, 5280) data = coon.recv(1024)#收消息,接收到的是bytes格式的 1024是接收的字节个数 print("from client :%s"%data) coon.send(data.upper())#发消息,发的也是bytes格式的 coon.close()#挂电话 phone.close()#关机
# 简版,客户端 import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8080))#拨通电话 client.send("hello".encode("utf-8"))#发消息,发的也是bytes二进制格式的 data = client.recv(1024)#收消息 print(data) client.close()
改进后的
# 改进版 # socket服务端 import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#相当于买手机 phone.bind(("127.0.0.1",8080))#插卡,必须是元组的形式 phone.listen(5)#开机5代表等待接听的个数 while True:#连接循环,当前通话结束后可以连接下个客户端 coon,addr = phone.accept() while True:#通讯循环 try:#尝试的去检查下面的代码是否报异常(防止在tcp正在连接通信时一方突然中断出现报错) data = coon.recv(1024) if not data:break#针对linux,客户端断开的异常处理(在linux上如果客户端突然断开,服务端会一直收到空进入死循环) print("from client :%s"%data) coon.send(data.upper()) except Exception:#如果出现异常就会捕捉,然后执行break代码Exception是万能的异常,意思是上面代码出现任何异常都能捕捉 break coon.close()#挂电话 phone.close()#关机
# 改进,客户端 import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8080))#拨通电话 while True:#通讯循环 msg = input(">>:") if not msg:continue#防止客户端发送空而卡在recv这 client.send(msg.encode("utf-8"))#发消息,发的也是bytes二进制格式的 data = client.recv(1024)#收消息 print(data) client.close()
远程执行命令(客户端发送命令服务端执行并返回结果)
# socket服务端 import socket import subprocess phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(("127.0.0.1",8080)) phone.listen(5) while True: conn,addr = phone.accept() while True:#通讯循环 try: cmd = conn.recv(1024) if not cmd:break print("from client msg",cmd) res = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err = res.stderr.read() if err: back_msg = err else: back_msg = res.stdout.read() conn.send(back_msg) except Exception: break conn.close() phone.close()
# 客户端 import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8080)) while True:#通讯循环 cmd = input(">>:").strip() if not cmd:continue client.send(cmd.encode("utf-8")) res = client.recv(1024) print(res.decode("gbk")) client.close()
粘包
须知:只有TCP有粘包现象,UDP永远不会粘包
首先需要掌握一个socket收发消息的原理
原理:无论是服务端还是客户端发送接收消息都是从自己的本地缓存中操作的,缓存中的数据是操作系统帮助其发送还是接收
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
解决粘包的方法
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
(先发个固定的数据长度,然后根据长度在取数据)
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
struct的用法
import struct x=struct.pack("i",1243)#pack是打包的意思,i是固定的字节4,是把整型转化成固定的字节4 # x1 = struct.pack("i","wqrwrqwrqewrwetqewtetgewqwtrtcxvxc")只能固定整型 # print(x1)报错 print(x) a = struct.unpack("i",x)#解包,解包出来后是一个元组 print(a) # >>:(1243,) print(a[0])#取元组的第一个元素就可以找到整型数据了
解决方法
# socket服务端 import socket import subprocess import struct phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(("127.0.0.1",8080)) phone.listen(5) while True: conn,addr = phone.accept() while True:#通讯循环 try: cmd = conn.recv(1024) if not cmd:break print("from client msg",cmd) res = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err = res.stderr.read() if err: back_msg = err else: back_msg = res.stdout.read() conn.send(struct.pack("i",len(back_msg))) #发送4个字节的数据内包含back_msg数据的真实长度 conn.sendall(back_msg)#sendall是循环的调用send,当数据过大时send一次性发过去缓存会成不下 except Exception: break conn.close() phone.close()
# 客户端 import socket import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8080)) while True:#通讯循环 cmd = input(">>:").strip() if not cmd:continue client.send(cmd.encode("utf-8")) data = client.recv(4) data_size = struct.unpack("i",data)[0] recv_size = 0 res_byte = b"" if recv_size< data_size: res = client.recv(1024) res_byte+=res recv_size+=len(res) print(recv_size.decode("gbk")) client.close()