socket 网络编程
一.楔子
如果这两个程序之间想要传递一个数据,你要怎么做呢?
创建一个文件,把a.py想要传递的内容写到文件中,然后b.py从这个文件中读取内容就可以了。
但是当你的a.py和b.py分别在不同电脑上的时候,你要怎么办呢?
类似的机制有计算机网盘,qq等等。我们可以在我们的电脑上和别人聊天,可以在自己的电脑上向网盘中上传、下载内容。这些都是两个程序在通信。
二.软件开发的架构
我们了解的涉及到两个程序之间通讯的应用大致可以分为两种:
第一种是应用类:qq、微信、网盘、优酷这一类是属于需要安装的桌面应用
第二种是web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用
这些应用的本质其实都是两个程序之间的通讯。而这两个分类又对应了两个软件开发的架构~
1.C/S架构
C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。
这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。
2.B/S架构
B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。
Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查。
1.一个程序如何在网络上找到另一个程序?
首先,程序必须要启动,其次,必须有这台机器的地址,我们都知道我们人的地址大概就是国家\省\市\区\街道\楼\门牌号这样字。那么每一台联网的机器在网络上也有自己的地址,它的地址是怎么表示的呢?
就是使用一串数字来表示的,例如:100.4.5.6

IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。

"端口"是英文port的意译,可以认为是设备与外界通讯交流的出口。
因此ip地址精确到具体的一台电脑,而端口精确到具体的程序。
2.osi七层模型
引子
须知一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷啥的)
如果你要跟别人一起玩,那你就需要上网了,什么是互联网?
互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。
osi七层模型
人们按照分工不同把互联网协议从逻辑上划分了层级:
3.socket概念
socket层
理解socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
3.套接字(socket)的发展史
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
4.tcp协议和udp协议
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
我知道说这些你们也不懂,直接上图。
四.套接字(socket)初使用
基于TCP协议的socket
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
server端
1 2 3 4 5 6 7 8 9 10 | import socket sk = socket.socket() sk.bind(( '127.0.0.1' , 8898 )) #把地址绑定到套接字 sk.listen() #监听链接 conn,addr = sk.accept() #接受客户端链接 ret = conn.recv( 1024 ) #接收客户端信息 print (ret) #打印客户端信息 conn.send(b 'hi' ) #向客户端发送信息 conn.close() #关闭客户端套接字 sk.close() #关闭服务器套接字(可选) |
client端
1 2 3 4 5 6 7 | import socket sk = socket.socket() # 创建客户套接字 sk.connect(( '127.0.0.1' , 8898 )) # 尝试连接服务器 sk.send(b 'hello!' ) ret = sk.recv( 1024 ) # 对话(发送/接收) print (ret) sk.close() # 关闭客户套接字 |
问题:在重启服务端时可能会遇到
解决方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #加入一条socket配置,重用ip和端口 import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.setsockopt(SOL_SOCKET,SO_REUSEADDR, 1 ) #就是它,在bind前加 sk.bind(( '127.0.0.1' , 8898 )) #把地址绑定到套接字 sk.listen() #监听链接 conn,addr = sk.accept() #接受客户端链接 ret = conn.recv( 1024 ) #接收客户端信息 print (ret) #打印客户端信息 conn.send(b 'hi' ) #向客户端发送信息 conn.close() #关闭客户端套接字 sk.close() #关闭服务器套接字(可选) |
基于UDP协议的socket
udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接
server端
1 2 3 4 5 6 7 | import socket udp_sk = socket.socket( type = socket.SOCK_DGRAM) #创建一个服务器的套接字 udp_sk.bind(( '127.0.0.1' , 9000 )) #绑定服务器套接字 msg,addr = udp_sk.recvfrom( 1024 ) print (msg) udp_sk.sendto(b 'hi' ,addr) # 对话(接收与发送) udp_sk.close() # 关闭服务器套接字 |
client端
1 2 3 4 5 6 | import socket ip_port = ( '127.0.0.1' , 9000 ) udp_sk = socket.socket( type = socket.SOCK_DGRAM) udp_sk.sendto(b 'hello' ,ip_port) back_msg,addr = udp_sk.recvfrom( 1024 ) print (back_msg.decode( 'utf-8' ),addr) |
qq聊天
server端
1 2 3 4 5 6 7 8 9 10 11 12 | #_*_coding:utf-8_*_ 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]的一条消息:\033[1;44m%s\033[0m' % (addr[ 0 ],addr[ 1 ],qq_msg.decode( 'utf-8' ))) back_msg = input ( '回复消息: ' ).strip() udp_server_sock.sendto(back_msg.encode( 'utf-8' ),addr) |
client端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #_*_coding:utf-8_*_ import socket BUFSIZE = 1024 udp_client_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic = { '金老板' :( '127.0.0.1' , 8081 ), '哪吒' :( '127.0.0.1' , 8081 ), 'egg' :( '127.0.0.1' , 8081 ), 'yuan' :( '127.0.0.1' , 8081 ), } while True : qq_name = input ( '请选择聊天对象: ' ).strip() while True : msg = input ( '请输入消息,回车发送,输入q结束和他的聊天: ' ).strip() if msg = = 'q' : 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]的一条消息:\033[1;44m%s\033[0m' % (addr[ 0 ],addr[ 1 ],back_msg.decode( 'utf-8' ))) udp_client_socket.close() |
socket参数的详解
1 | socket.socket(family = AF_INET, type = SOCK_STREAM,proto = 0 ,fileno = None ) |
创建socket对象的参数说明:
、
五.黏包
黏包现象
基于tcp协议实现的黏包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #_*_coding:utf-8_*_ from socket import * import subprocess ip_port = ( '127.0.0.1' , 8888 ) BUFSIZE = 1024 tcp_socket_server = socket(AF_INET,SOCK_STREAM) tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR, 1 ) tcp_socket_server.bind(ip_port) tcp_socket_server.listen( 5 ) while True : conn,addr = tcp_socket_server.accept() print ( '客户端' ,addr) while True : cmd = conn.recv(BUFSIZE) if len (cmd) = = 0 : break res = subprocess.Popen(cmd.decode( 'utf-8' ),shell = True , stdout = subprocess.PIPE, stdin = subprocess.PIPE, stderr = subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() conn.send(stderr) conn.send(stdout) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #_*_coding:utf-8_*_ import socket BUFSIZE = 1024 ip_port = ( '127.0.0.1' , 8888 ) s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = s.connect_ex(ip_port) while True : msg = input ( '>>: ' ).strip() if len (msg) = = 0 : continue if msg = = 'quit' : break s.send(msg.encode( 'utf-8' )) act_res = s.recv(BUFSIZE) print (act_res.decode( 'utf-8' ),end = '') |
基于udp协议实现的黏包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #_*_coding:utf-8_*_ from socket import * import subprocess ip_port = ( '127.0.0.1' , 9000 ) bufsize = 1024 udp_server = socket(AF_INET,SOCK_DGRAM) udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR, 1 ) udp_server.bind(ip_port) while True : #收消息 cmd,addr = udp_server.recvfrom(bufsize) print ( '用户命令----->' ,cmd) #逻辑处理 res = subprocess.Popen(cmd.decode( 'utf-8' ),shell = True ,stderr = subprocess.PIPE,stdin = subprocess.PIPE,stdout = subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() #发消息 udp_server.sendto(stderr,addr) udp_server.sendto(stdout,addr) udp_server.close() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from socket import * ip_port = ( '127.0.0.1' , 9000 ) bufsize = 1024 udp_client = socket(AF_INET,SOCK_DGRAM) while True : msg = input ( '>>: ' ).strip() udp_client.sendto(msg.encode( 'utf-8' ),ip_port) err,addr = udp_client.recvfrom(bufsize) out,addr = udp_client.recvfrom(bufsize) if err: print ( 'error : %s' % err.decode( 'utf-8' ),end = '') if out: print (out.decode( 'utf-8' ), end = '') |
注意:只有TCP有粘包现象,UDP永远不会粘包
黏包的解决方案
解决方案一
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
server端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #_*_coding:utf-8_*_ import socket,subprocess ip_port = ( '127.0.0.1' , 8080 ) 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() print ( '客户端' ,addr) while True : msg = conn.recv( 1024 ) if not msg: break res = subprocess.Popen(msg.decode( 'utf-8' ),shell = True ,\ stdin = subprocess.PIPE,\ stderr = subprocess.PIPE,\ stdout = subprocess.PIPE) err = res.stderr.read() if err: ret = err else : ret = res.stdout.read() data_length = len (ret) conn.send( str (data_length).encode( 'utf-8' )) data = conn.recv( 1024 ).decode( 'utf-8' ) if data = = 'recv_ready' : conn.sendall(ret) conn.close() |
client端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #_*_coding:utf-8_*_ import socket,time s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = s.connect_ex(( '127.0.0.1' , 8080 )) while True : msg = input ( '>>: ' ).strip() if len (msg) = = 0 : continue if msg = = 'quit' : break s.send(msg.encode( 'utf-8' )) length = int (s.recv( 1024 ).decode( 'utf-8' )) s.send( 'recv_ready' .encode( 'utf-8' )) send_size = 0 recv_size = 0 data = b'' while recv_size < length: data + = s.recv( 1024 ) recv_size + = len (data) print (data.decode( 'utf-8' )) |
存在的问题: 程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
解决方案进阶
刚刚的方法,问题在于我们我们在发送
我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
struct模块
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
1 2 3 | >>> struct.pack( 'i' , 1111111111111 ) struct.error: 'i' format requires - 2147483648 < = number < = 2147483647 #这个是范围 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import json,struct #假设通过客户端上传1T:1073741824000的文件a.txt #为避免粘包,必须自定制报头 header = { 'file_size' : 1073741824000 , 'file_name' : '/a/b/c/d/e/a.txt' , 'md5' : '8f6fbf8347faa4924a76856701edb0f3' } #1T数据,文件路径和md5值 #为了该报头能传送,需要序列化并且转为bytes head_bytes = bytes(json.dumps(header),encoding = 'utf-8' ) #序列化并转成bytes,用于传输 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 head_len_bytes = struct.pack( 'i' , len (head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 #客户端开始发送 conn.send(head_len_bytes) #先发报头的长度,4个bytes conn.send(head_bytes) #再发报头的字节格式 conn.sendall(文件内容) #然后发真实内容的字节格式 #服务端开始接收 head_len_bytes = s.recv( 4 ) #先收报头4个bytes,得到报头长度的字节格式 x = struct.unpack( 'i' ,head_len_bytes)[ 0 ] #提取报头的长度 head_bytes = s.recv(x) #按照报头长度x,收取报头的bytes格式 header = json.loads(json.dumps(header)) #提取报头 #最后根据报头的内容提取真实的数据,比如 real_data_len = s.recv(header[ 'file_size' ]) s.recv(real_data_len) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #_*_coding:utf-8_*_ #http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html __author__ = 'Linhaifeng' import struct import binascii import ctypes values1 = ( 1 , 'abc' .encode( 'utf-8' ), 2.7 ) values2 = ( 'defg' .encode( 'utf-8' ), 101 ) s1 = struct.Struct( 'I3sf' ) s2 = struct.Struct( '4sI' ) print (s1.size,s2.size) prebuffer = ctypes.create_string_buffer(s1.size + s2.size) print ( 'Before : ' ,binascii.hexlify(prebuffer)) # t=binascii.hexlify('asdfaf'.encode('utf-8')) # print(t) s1.pack_into(prebuffer, 0 , * values1) s2.pack_into(prebuffer,s1.size, * values2) print ( 'After pack' ,binascii.hexlify(prebuffer)) print (s1.unpack_from(prebuffer, 0 )) print (s2.unpack_from(prebuffer,s1.size)) s3 = struct.Struct( 'ii' ) s3.pack_into(prebuffer, 0 , 123 , 123 ) print ( 'After pack' ,binascii.hexlify(prebuffer)) print (s3.unpack_from(prebuffer, 0 )) |
使用struct解决黏包
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
服务端自制报头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import socket,struct,json import subprocess phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1 ) #就是它,在bind前加 phone.bind(( '127.0.0.1' , 8080 )) phone.listen( 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() conn.send(struct.pack( 'i' , len (back_msg))) #先发back_msg的长度 conn.sendall(back_msg) #在发真实的内容 conn.close() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #_*_coding:utf-8_*_ import socket,time,struct s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = s.connect_ex(( '127.0.0.1' , 8080 )) while True : msg = input ( '>>: ' ).strip() if len (msg) = = 0 : continue if msg = = 'quit' : break s.send(msg.encode( 'utf-8' )) l = s.recv( 4 ) x = struct.unpack( 'i' ,l)[ 0 ] print ( type (x),x) # print(struct.unpack('I',l)) r_s = 0 data = b'' while r_s < x: r_d = s.recv( 1024 ) data + = r_d r_s + = len (r_d) # print(data.decode('utf-8')) print (data.decode( 'gbk' )) #windows默认gbk编码 |
我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import socket,struct,json import subprocess phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1 ) #就是它,在bind前加 phone.bind(( '127.0.0.1' , 8080 )) phone.listen( 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() |
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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' )) 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编码 |
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 2025成都.NET开发者Connect圆满结束
· Ollama本地部署大模型总结
· langchain0.3教程:从0到1打造一个智能聊天机器人
· 用一种新的分类方法梳理设计模式的脉络
· C 语言内存布局深度剖析:从栈到堆,你真的了解吗?