Python基础之(Socket编程)
一、什么是Socket
Socket又称为套接字,它是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
UNIX BSD发明了socket这种东西,socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用,通过系统调用我们可以方便的使用磁盘、内存,而无需自己去进行磁盘读写,内存管理。socket其实也是一样的东西,就是提供了tcp/ip协议的抽象,对外提供了一套接口,通过这个接口就可以统一、方便的使用tcp/ip协议的功能了。从使用上面看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。 所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
二、套接字发展及分类
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
三、套接字的工作流程(基于TCP和 UDP两个协议)
3.1、TCP与UDP
TCP(Transmission Control Protocol)可靠的、面向连接的协议。传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小)。一对一、一对多、多对一、多对多、面向报文(数据包),尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)
3.2、TCP协议下的Socket
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
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) #也可以'from module import *'语句,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() 关闭套接字
3.2.1、TCP socket示例三连
简单一次发送接收版:
#最简单版 #server端 import socket tcpserver = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #建立 tcpserver.bind(("127.0.0.1",10000)) #绑定 tcpserver.listen(5) #监听 conn,addr = tcpserver.accept() #阻塞等待连接 data = conn.recv(1024) #接收 print(data.decode("utf-8")) conn.send(data.upper()) #发送 conn.close() #关闭连接 tcpserver.close() #关闭 #client端 import socket tcpclient = socket.socket(socket.AF_INET,socket.SOCK_STREAM) tcpclient.connect(("127.0.0.1",10000)) #连接 data=input(">:") tcpclient.send(data.encode("utf-8")) #发送 ret=tcpclient.recv(1024) #接收 print(ret.decode("utf-8")) tcpclient.close()
执行结果:
循环接收发送信息示例:
#循环接收发送信息版 #server端 import socket tcpserver = socket.socket(socket.AF_INET,socket.SOCK_STREAM) tcpserver.bind(("127.0.0.1", 10000)) tcpserver.listen(5) while True: conn,addr = tcpserver.accept() print(conn) while True: try: data = conn.recv(1024) print(data.decode("utf-8")) conn.send(data.upper()) except Exception: break conn.close() tcpserver.close() #客户端 import socket tcpclient = socket.socket(socket.AF_INET,socket.SOCK_STREAM) tcpclient.connect(("127.0.0.1",10000)) while 1: while 1: data = input('>>>').strip() tcpclient.send(data.encode('utf-8')) ret = tcpclient.recv(1024) print(ret.decode('utf-8'))
执行结果:
远程执行结果返回示例:
(ret.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果)
#server端 import socket import subprocess tcpserver = socket.socket(socket.AF_INET,socket.SOCK_STREAM) tcpserver.bind(("127.0.0.1", 10000)) tcpserver.listen(5) while True: conn,addr = tcpserver.accept() print(conn) while True: try: data = conn.recv(1024) ret=subprocess.Popen(data.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) error = ret.stderr.read() if error: cmd_ret = error else: cmd_ret=ret.stdout.read() conn.send(cmd_ret) except Exception: break conn.close() tcpserver.close() #客户端 import socket tcpclient = socket.socket(socket.AF_INET,socket.SOCK_STREAM) tcpclient.connect(("127.0.0.1",10000)) while 1: while 1: data = input('>>>').strip() tcpclient.send(data.encode('utf-8')) ret = tcpclient.recv(1024) print(ret.decode('gbk'))
执行结果:
3.3、UDP协议下的 Socket
UDP下的socket通讯流程:先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),recvform接收消息,这个消息有两项,消息内容和对方客户端的地址,然后回复消息时也要带着你收到的这个客户端的地址,发送回去,最后关闭连接,一次交互就结束了
3.3.1、Udp socket 示例二连
循环发送接收消息版:
#server端 import socket ip_port=('127.0.0.1',10001) udp_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server.bind(ip_port) while True: msg,addr=udp_server.recvfrom(1024) print(msg,addr) udp_server.sendto(msg.upper(),addr) udp_server.close() #客户端 import socket ip_port=('127.0.0.1',10001) udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) msg,addr=udp_client.recvfrom(1024) print(msg.decode('utf-8'),addr) udp_client.close()
发送接收时间示例:
#server端: import socket,time ip_port=('127.0.0.1',10001) udp_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server.bind(ip_port) while True: msg,addr=udp_server.recvfrom(1024) if not msg: ret = "%Y-%m-%d %X" else: ret=msg.decode("utf-8") cmd_ret = time.strftime(ret) udp_server.sendto(cmd_ret.encode("utf-8"),addr) udp_server.close() #客户端: import socket ip_port=('127.0.0.1',10001) udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) msg,addr=udp_client.recvfrom(1024) print(msg.decode('utf-8'))
执行结果:
注意点:
- udp是无链接的,先启动哪一端都不会报错,而tcp需要先启动server端
- 由于udp无连接,所以可以同时多个客户端去跟服务端通信,而tcp同时只能有一个客户端与服务端连接,其他客户端只能等待中,直到连接中客户端下线
四、粘包
4.1、Socket 缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取
4.2、粘包的原因
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
所以所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。并且只有TCP有粘包现象,UDP永远不会粘包!
粘包的两种情况:
- 接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
- 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)
4.3、解决粘包的方法
粘包问题的根源在于接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是如何让发送端在发送数据前先把自己将要发送的数据大小让接收端知晓,然后再来一个循环接收完所有数据
#服务端 import socket,subprocess ip_port=('127.0.0.1',10001) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept() while True: try: msg=conn.recv(1024) res=subprocess.Popen(msg.decode('utf-8'),shell=True,stdin=subprocess.PIPE,stderr=subprocess.PIPE,stdout=subprocess.PIPE) error=res.stderr.read() if error: ret=error else: ret=res.stdout.read() data_length=len(ret) print(data_length) conn.send(str(data_length).encode('utf-8')) #发送长度 data=conn.recv(1024).decode('utf-8') if data == 'ready': #确认 conn.sendall(ret) #发送全部数据 except Exception: break conn.close() s.close() #客户端 import socket s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('127.0.0.1',10001)) while True: msg=input('>>: ').strip() s.send(msg.encode('utf-8')) length=int(s.recv(1024).decode('utf-8')) #接收长度 print(length) s.send('ready'.encode('utf-8')) recv_size=0 data = b"" while recv_size < length: #如果长度不够就一直接收,直到完成 r_m = s.recv(1024) data+= r_m recv_size+=len(r_m) print(recv_size) print(data.decode('gbk'))
另一种解决粘包的方法即是:将总数据大小封装成固定大小后和数据分别发,接收方先接收封装的总数据大小,然后再接收数据(效率更高)
#server 端 import socket,subprocess,struct ip_port=('127.0.0.1',10001) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept() while True: try: msg=conn.recv(1024) res=subprocess.Popen(msg.decode('utf-8'),shell=True,stdin=subprocess.PIPE,stderr=subprocess.PIPE,stdout=subprocess.PIPE) error=res.stderr.read() if error: ret=error else: ret=res.stdout.read() length=len(ret) conn.send(struct.pack("i",length)) #发送封装好的固定长度 conn.sendall(ret) #发送全部数据 except Exception: break conn.close() s.close() #client端 import socket,struct s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('127.0.0.1',10001)) while True: msg=input('>>: ').strip() s.send(msg.encode('utf-8')) l=s.recv(4) print(struct.unpack("i",l)) length = struct.unpack("i",l)[0] #解析封装好的固定长度 print(length) recv_size=0 data = b"" while recv_size < length: #如果长度不够就一直接收,直到完成 r_m = s.recv(1024) data+= r_m recv_size+=len(r_m) print(recv_size) print(data.decode('gbk'))
补充:struct模块说明:
用处:
- 按照指定格式将Python数据转换为字符串,该字符串为字节流,如网络传输时,不能传输int,此时先将int转化为字节流,然后再发送;
- 按照指定格式将字节流转换为Python指定的数据类型;
- 处理二进制数据,如果用struct来处理文件的话,需要用’wb’,’rb’以二进制(字节流)写,读的方式来处理文件;
- 处理c语言中的结构体;
struct模块中的函数
函数 | return | explain |
---|---|---|
pack(fmt,v1,v2…) | string | 按照给定的格式(fmt),把数据转换成字符串(字节流),并将该字符串返回. |
pack_into(fmt,buffer,offset,v1,v2…) | None | 按照给定的格式(fmt),将数据转换成字符串(字节流),并将字节流写入以offset开始的buffer中.(buffer为可写的缓冲区,可用array模块) |
unpack(fmt,v1,v2…..) | tuple | 按照给定的格式(fmt)解析字节流,并返回解析结果 |
pack_from(fmt,buffer,offset) | tuple | 按照给定的格式(fmt)解析以offset开始的缓冲区,并返回解析结果 |
calcsize(fmt) | size of fmt | 计算给定的格式(fmt)占用多少字节的内存,注意对齐方式 |
格式符
格式符 | C语言类型 | Python类型 | Standard size |
---|---|---|---|
x | pad byte(填充字节) | no value | |
c | char | string of length 1 | 1 |
b | signed char | integer | 1 |
B | unsigned char | integer | 1 |
? | _Bool | bool | 1 |
h | short | integer | 2 |
H | unsigned short | integer | 2 |
i | int | integer | 4 |
I(大写的i) | unsigned int | integer | 4 |
l(小写的L) | long | integer | 4 |
L | unsigned long | long | 4 |
q | long long | long | 8 |
Q | unsigned long long | long | 8 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | string | |
p | char[] | string | |
P | void * | long |
五、SocketServer 实现并发
基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环。而socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)
#server 端 import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): print(self.request) while True: try: self.data = self.request.recv(1024).strip() print(self.data) self.request.sendall(self.data.upper()) except Exception: break if __name__ == "__main__": HOST, PORT = "127.0.0.1", 10001 # 创建一个server, 将服务地址绑定到127.0.0.1:10001 (TCP socket) # server = socketserver.TCPServer((HOST, PORT),Myserver) #(多线程) server = socketserver.ThreadingTCPServer((HOST, PORT), Myserver) #(多进程) #server=socketserver.ForkingTCPServer((host,port),myserver) # 让server永远运行下去,除非强制停止程序 server.serve_forever() #客户端 import socket s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('127.0.0.1',10001)) while True: msg=input('>>: ').strip() s.send(msg.encode('utf-8')) data=s.recv(1024) print(data.decode('utf-8'))
执行结果:
补充udp (不要这样玩,因为每新建一个连接就创建一个实例):
#udp socketserver import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): print(self.request) self.data = self.request[0] print(self.data) self.request[1].sendto(self.request[0].upper(), self.client_address) if __name__ == "__main__": HOST, PORT = "127.0.0.1", 10001 # server = socketserver.TCPServer((HOST, PORT),Myserver) server = socketserver.ThreadingUDPServer((HOST, PORT), Myserver) # 让server永远运行下去,除非强制停止程序 server.serve_forever() #客户端 import socket ip_port=('127.0.0.1',10001) udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) msg,addr=udp_client.recvfrom(1024) print(msg.decode('utf-8'),addr) udp_client.close()
基于tcp的socketserver我们自己定义的类中的
- self.server即套接字对象
- self.request即一个链接 (self.request = conn)
- self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中的
- self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
- self.client_address即客户端地址
六、客户端连接合法示例
#server端 import socketserver import hmac,os secret_key=b'Crazyjump' def conn_auth(conn): ''' 认证客户端链接 ''' print('开始验证新链接的合法性') msg=os.urandom(16) #随机生成16个字节的串 print(msg) conn.sendall(msg) h=hmac.new(secret_key,msg) digest=h.digest() respone=conn.recv(len(digest)) return hmac.compare_digest(respone,digest) def data_handler(conn,bufsize=1024): if not conn_auth(conn): print('该链接不合法,关闭') conn.close() return print('链接合法,开始通信') while True: try: data=conn.recv(bufsize) conn.sendall(data.upper()) except Exception: break class Myserver(socketserver.BaseRequestHandler): def handle(self): data_handler(self.request) if __name__ == "__main__": HOST, PORT = "127.0.0.1", 10001 server = socketserver.ThreadingTCPServer((HOST, PORT), Myserver) server.serve_forever() #(单) #def server_handler(ip_port,bufsize,backlog=5): ''' 只处理链接 ''' # tcp_socket_server=socket(AF_INET,SOCK_STREAM) #tcp_socket_server.bind(ip_port) # tcp_socket_server.listen(backlog) #while True: # conn,addr=tcp_socket_server.accept() #print('新连接[%s:%s]' %(addr[0],addr[1])) #data_handler(conn,bufsize) #客户端 import hmac,os,socket secret_key=b'Crazyjump' def conn_auth(conn): ''' 验证客户端到服务器的链接 ''' msg=conn.recv(16) h=hmac.new(secret_key,msg) digest=h.digest() conn.sendall(digest) def client_handler(ip_port): tcp_socket_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) tcp_socket_client.connect(ip_port) conn_auth(tcp_socket_client) while True: data=input('>>: ').strip() tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(1024) print(respone.decode('utf-8')) tcp_socket_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',10001) client_handler(ip_port)