我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。
能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
在传输层中有两个协议,一个是TCP,一个是UDP,这两个一个是建立连接的,一个是无连接的。关于这个两个协议的详细内容很多,学习起来也很复杂,所以在使用的时候,我们常用的是Socket。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序而程序的pid是同一台机器上不同进程或者线程的标识。
套接字的发展史及分类
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
套接字的工作流程
下面会介绍TCP和UDP两种连接方式,TCP是面向连接的,而UDP是无连接的。
基于TCP的套接字
下图中,将套接字的流程用生活中的一个例子进行了说明
下面是服务端和客户端的代码
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机,第一个参数表示基于网络的套接字,第二个表示TCP协议 phone.bind(('127.0.0.1', 8000)) # 绑定手机卡,参数为一个元组,要绑定ip和端口。 phone.listen(5) # 待机,参数5表示可以同时和5和客户端建立链接 print('---->') conn, addr = phone.accept() # 等电话,收到的返回值是一个元组,第一个是链接,第二个个是发送过来信息的客户端的地址。 msg = conn.recv(1024) # 收消息,参数表示backlog多大,也就是最大可以挂起几个链接 print('客户端发来的消息是: ', msg) conn.send(msg.upper()) # 发消息 conn.close() phone.close()
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(('127.0.0.1', 8000)) # 拨通电话,客户端的connect和服务端的accept就完成了三次握手 phone.send('hello'.encode('utf-8')) # 发消息, 收发消息在网络传输时都要是二进制,所以转成二进制。 data = phone.recv(1024) print('收到服务端的发来的消息:', data)
改进版
import socket ip_port=('127.0.0.1',8081)#电话卡 BUFSIZE=1024 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机 s.bind(ip_port) #手机插卡 s.listen(5) #手机待机 while True: #新增接收链接循环,可以不停的接电话 conn,addr=s.accept() #手机接电话 # print(conn) # print(addr) print('接到来自%s的电话' %addr[0]) while True: #新增通信循环,可以不断的通信,收发消息 msg=conn.recv(BUFSIZE) #听消息,听话 # if len(msg) == 0:break #如果不加,那么正在链接的客户端突然断开,recv便不再阻塞,死循环发生 print(msg,type(msg)) conn.send(msg.upper()) #发消息,说话 conn.close() #挂电话 s.close() #手机关机
import socket ip_port=('127.0.0.1',8081) BUFSIZE=1024 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect_ex(ip_port) #拨电话 while True: #新增通信循环,客户端可以不断发收消息 msg=input('>>: ').strip() if len(msg) == 0:continue s.send(msg.encode('utf-8')) #发消息,说话(只能发送字节类型) feedback=s.recv(BUFSIZE) #收消息,听话 print(feedback.decode('utf-8')) s.close() #挂电话
基于UDP的套接字
UDP是面向无连接的,所有先启动客户端或者先启动服务端都是可以的。
import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server_client.bind(ip_port) while True: msg,addr=udp_server_client.recvfrom(BUFSIZE) print(msg,addr) udp_server_client.sendto(msg.upper(),addr)
import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>: ').strip() if not msg:continue udp_server_client.sendto(msg.encode('utf-8'),ip_port) back_msg,addr=udp_server_client.recvfrom(BUFSIZE) print(back_msg.decode('utf-8'),addr)
一些娱乐:
聊天
import socket ip_port = ('127.0.0.1', 8081) udp_server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 买手机 udp_server_sock.bind(ip_port) while True: qq_msg, addr = udp_server_sock.recvfrom(1024) print('来自[%s:%s]的一条消息: 33[1;44m%s 33[0m' % (addr[0], addr[1], qq_msg.decode('utf-8'))) back_msg = input('回复消息: ').strip() udp_server_sock.sendto(back_msg.encode('utf-8'), addr)
import socket BUFSIZE = 1024 udp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) qq_name_dic = { 'A': ('127.0.0.1', 8081), 'B': ('127.0.0.1', 8081), 'C': ('127.0.0.1', 8081), 'D': ('127.0.0.1', 8081), } while True: qq_name = input('请选择聊天对象: ').strip() while True: msg = input('请输入消息,回车发送: ').strip() if msg == 'quit': break if not msg or not qq_name or qq_name not in qq_name_dic: continue udp_client_socket.sendto(msg.encode('utf-8'), qq_name_dic[qq_name]) back_msg, addr = udp_client_socket.recvfrom(BUFSIZE) print('来自[%s:%s]的一条消息: 33[1;44m%s 33[0m' % (addr[0], addr[1], back_msg.decode('utf-8'))) udp_client_socket.close()
import socket BUFSIZE = 1024 udp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) qq_name_dic = { 'A': ('127.0.0.1', 8081), 'B': ('127.0.0.1', 8081), 'C': ('127.0.0.1', 8081), 'D': ('127.0.0.1', 8081), } while True: qq_name = input('请选择聊天对象: ').strip() while True: msg = input('请输入消息,回车发送: ').strip() if msg == 'quit': break if not msg or not qq_name or qq_name not in qq_name_dic: continue udp_client_socket.sendto(msg.encode('utf-8'), qq_name_dic[qq_name]) back_msg, addr = udp_client_socket.recvfrom(BUFSIZE) print('来自[%s:%s]的一条消息: 33[1;44m%s 33[0m' % (addr[0], addr[1], back_msg.decode('utf-8'))) udp_client_socket.close()
常用函数
服务端套接字函数
- 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() 创建一个与该套接字相关的文件
问题
我们重启服务端的时候,可能会遇到Address already in use,提示我们端口被占用了。这是由于服务端仍然处于四次挥手的tme_wait状态在占用地址(tcp的三次握手和四次挥手中的一种状态)。
解决办法
#加入一条socket配置,重用ip和端口 phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080))
网上搜罗到的一个方案: 发现系统存在大量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 时间
粘包
首先演示两种粘包情况:
情况一:
from socket import * ip_port = ('127.0.0.1', 8080) back_log = 5 buffer_size = 1024 tcp_server = socket(AF_INET, SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) conn, addr = tcp_server.accept() data1 = conn.recv(10) print('第一次数据', data1) data2 = conn.recv(5) print('第2次数据', data2) data3 = conn.recv(5) print('第3次数据', data3)
from socket import * ip_port = ('127.0.0.1', 8080) back_log = 5 buffer_size = 1024 tcp_client = socket(AF_INET, SOCK_STREAM) tcp_client.connect(ip_port) tcp_client.send('hello'.encode('utf-8')) tcp_client.send('world'.encode('utf-8')) tcp_client.send('egon'.encode('utf-8'))
最后服务端接收到的结果:
情况二:
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()
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 world'.encode('utf-8'))
粘包现象说明
上面的两个例子就是粘包的两种现象,而详细产生这两种情况的原因下面会进行说明。
在开始说明之前,先分别说一下TCP和UDP套接字:
- TCP(transport control protocol,传输控制协议)
- 是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)
- 是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
TCP和UDP的特点:
-
recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
-
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。
- udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
- tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
- 当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
从上面的说明就可以解释一开始的两种情况了。其实总而言之,言而总之,所谓的粘包就是接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
解决粘包问题
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
思路就是做一个报头,报头是用一个字典来包含要发送数据的详细信息。通过json序列化,在通过struck函数将序列化好的数据长度打包成4个字节。先发送给接收端。然后在开始发送。
下面是两个网上找的两个例子:
例子1:用tcp实现客户端发送shell命令,服务端执行并返回执行结果
import socket, struct, json import subprocess phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 建立socket连接 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 设置之后操作系统会在服务器socket被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。 phone.bind(('127.0.0.1', 8080)) # 绑定端口 phone.listen(5) # 允许有多少个未决(等待)的连接在队列中等待。作为一个约定,很多人设置为5。 while True: conn, addr = phone.accept() while True: cmd = conn.recv(1024) if not cmd: break print('cmd: %s' % cmd) res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err = res.stderr.read() print(err) if err: back_msg = err else: back_msg = res.stdout.read() headers = {'data_size': len(back_msg)} head_json = json.dumps(headers) head_json_bytes = bytes(head_json, encoding='utf-8') conn.send(struct.pack('i', len(head_json_bytes))) # 先发报头的长度 conn.send(head_json_bytes) # 再发报头 conn.sendall(back_msg) # 在发真实的内容 conn.close()
from socket import * import struct, json ip_port = ('127.0.0.1', 8080) client = socket(AF_INET, SOCK_STREAM) client.connect(ip_port) while True: cmd = input('>>: ') if not cmd: continue client.send(bytes(cmd, encoding='utf-8')) # 这里要说一下,这个tcp是流式协议,这样方便后面理解 head = client.recv(4) head_json_len = struct.unpack('i', head)[0] head_json = json.loads(client.recv(head_json_len).decode('utf-8')) data_len = head_json['data_size'] recv_size = 0 recv_data = b'' while recv_size < data_len: recv_data += client.recv(1024) recv_size += len(recv_data) print(recv_data.decode('utf-8')) # print(recv_data.decode('gbk')) #windows默认gbk编码
里面用到了一个subprocess模块,我网上找了一个比较详细的说明的:subprocess:可以在当前程序中执行其他程序或命令
例子2:用tcp实现文件
import socket import struct import json import subprocess import os class MYTCPServer: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding = 'utf-8' request_queue_size = 5 server_dir = 'file_upload' def __init__(self, server_address, bind_and_activate=True): """Constructor. May be extended, do not override.""" self.server_address = server_address self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): """Called by constructor to bind the socket. """ if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname() def server_activate(self): """Called by constructor to activate the server. """ self.socket.listen(self.request_queue_size) def server_close(self): """Called to clean-up the server. """ self.socket.close() def get_request(self): """Get the request and client address from the socket. """ return self.socket.accept() def close_request(self, request): """Called to clean up an individual request.""" request.close() def run(self): while True: self.conn, self.client_addr = self.get_request() print('from client ', self.client_addr) while True: try: head_struct = self.conn.recv(4) if not head_struct: break head_len = struct.unpack('i', head_struct)[0] head_json = self.conn.recv(head_len).decode(self.coding) head_dic = json.loads(head_json) print(head_dic) # head_dic={'cmd':'put','filename':'a.txt','filesize':123123} cmd = head_dic['cmd'] if hasattr(self, cmd): func = getattr(self, cmd) func(head_dic) except Exception: break def put(self, args): file_path = os.path.normpath(os.path.join( self.server_dir, args['filename'] )) filesize = args['filesize'] recv_size = 0 print('----->', file_path) with open(file_path, 'wb') as f: while recv_size < filesize: recv_data = self.conn.recv(self.max_packet_size) f.write(recv_data) recv_size += len(recv_data) print('recvsize:%s filesize:%s' % (recv_size, filesize)) tcpserver1 = MYTCPServer(('127.0.0.1', 8080)) tcpserver1.run() # 下列代码与本题无关 class MYUDPServer: """UDP server class.""" address_family = socket.AF_INET socket_type = socket.SOCK_DGRAM allow_reuse_address = False max_packet_size = 8192 coding = 'utf-8' def get_request(self): data, client_addr = self.socket.recvfrom(self.max_packet_size) return (data, self.socket), client_addr def server_activate(self): # No need to call listen() for UDP. pass def shutdown_request(self, request): # No need to shutdown anything. self.close_request(request) def close_request(self, request): # No need to close anything. pass
import socket import struct import json import os class MYTCPClient: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding = 'utf-8' request_queue_size = 5 def __init__(self, server_address, connect=True): self.server_address = server_address self.socket = socket.socket(self.address_family, self.socket_type) if connect: try: self.client_connect() except: self.client_close() raise def client_connect(self): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: inp = input(">>: ").strip() if not inp: continue l = inp.split() cmd = l[0] if hasattr(self, cmd): func = getattr(self, cmd) func(l) def put(self, args): cmd = args[0] filename = args[1] if not os.path.isfile(filename): print('file:%s is not exists' % filename) return else: filesize = os.path.getsize(filename) head_dic = {'cmd': cmd, 'filename': os.path.basename(filename), 'filesize': filesize} print(head_dic) head_json = json.dumps(head_dic) head_json_bytes = bytes(head_json, encoding=self.coding) head_struct = struct.pack('i', len(head_json_bytes)) self.socket.send(head_struct) self.socket.send(head_json_bytes) send_size = 0 with open(filename, 'rb') as f: for line in f: self.socket.send(line) send_size += len(line) print(send_size) else: print('upload successful') client = MYTCPClient(('127.0.0.1', 8080)) client.run()