socket

SOCKET :

socket本质上来说就是位于应用层与传输层之间的逻辑层,下图为tcp/ip四层,五层协议以及osi参考模型

img

一般现在用的就是五层模型,从两台计算机传输数据的角度来讲,物理层定义其计算机基本数据结构(0和1,也就是数据本身)。数据链路层定义了数据的传输(以mac地址为唯一标识),而这一层在局域网内通信方式也极其简单,就是通过广播发送到每一个局域网内的计算机上面,如非指定的接受者(mac地址),则丢弃。到网络层时就可以跨越其局域网的限制,通过网关设备的路由功能(通过ip地址进行路由),就可以找到互联网上的每一个设备进行传输通信,但其本质上还是数据链路层通信,网络层最主要是定位到互联网每一台主机(ARP协议最终将ip解析到mac地址)。到传输层时有两个协议(tcp和udp),tcp是稳定的传输协议,主要保证数据完整性,而udp是非稳定的传输协议,用于非重要数据传输,例如视频聊天,qq信息等。这里传输层定义了端口,可使用范围为1-65535。如果说网络层是用来定位到互联网中的某一台计算机的话,那么传输层则是定位到计算机中的某个应用且与其建立连接(三次握手,其建立连接本质上也是逻辑上的建立)。到应用层这里则是访问到应用中具体某个资源,例如/index.html。

应用程序位于其应用层,在应用程序发送数据时,需要将其封装打包成一段段的数据段送到传输层,然后传输层再封装成网络包,到链路层时又封装成数据帧,到物理层又封装成数据位(二进制,0和1)。到目标主机以后从物理层开始又一层一层的拆包往上传递,所以正常情况下程序员在编写程序时,应该把客户端和目标服务器五层的封装以及拆包写好,但是这个步骤太过于麻烦且重复性工作,所以有了socket。

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

有了socket接口以后程序员就只用关心程序本身,只要将C/S两边程序写好,直接调用已有的socket模块来帮程序员完成应用层以下真正实现传输的所有步骤。

img

socket流程:

img

图中每一个步骤就是socket的每一个提供函数(方法)。

客户端:

首先创建socket对象,连接服务器端,发送(请求)数据和接受数据,然后关闭套接字

服务器端:
首先也需要创建socket对象,然后绑定端口(服务器需要固定的端口绑定,客户端使用随机ip和端口进行通信即可),监听,监听完毕后accept等待客户端连接(这里可以设置半连接池),然后连接完毕以后发送数据,接受数据(发送接受先后顺序不定,看程序需要),最后关闭连接(关闭这个客户端的连接),但一般不会关闭服务器套接字连接。

socket for python:

tcp流程

tcp客户端:
创建客户端套接字对象
和服务端套接字建立连接
发送数据
接收数据
关闭客户端套接字
socket() -> connect() -> send() -> recv () ->close()
tcp服务器:
创建服务端端套接字对象
绑定端口号
设置监听
等待接受客户端的连接请求
接收数据
发送数据
关闭套接字
socket()->bind()->listen()->accept()->recv()-send()->close()

tcp客户端开发:

需要导入socket模块,然后根据步骤调用每一个方法即可,实例代码如下

tcp:

客户端:

1
2
3
4
5
6
7
8
9
10
import socket
SOCKET=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

SOCKET.connect(('localhost',8080))

SOCKET.send('你就是传说中的服务器吗?我是客户端'.encode('utf-8'))

information=SOCKET.recv(1024)
print(information.decode('utf-8'))
SOCKET.close()

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import socket

SOCKET=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

SOCKET.bind(('localhost',8080))

SOCKET.listen()

user,user_addr=SOCKET.accept()
print('正在处理客户端消息,客户端ip为:',user_addr)
information=user.recv(1024)
print(information.decode('utf-8'))

user.send('给客户端发送消息'.encode('utf-8'))

user.close()

SOCKET.close()

udp:

服务端:

1
2
3
4
5
6
7
8
import socket
server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(('127.0.0.1',8080))
while True:
data,ip_addr=server.recvfrom(1024)
print(data.decode('utf-8'))
server.sendto(data,ip_addr)
server.close()

客户端:

1
2
3
4
5
6
7
8
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg=input('>>>:').strip()
server.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
data, ip_addr = server.recvfrom(1024)
print(data.decode('utf-8'))
server.close()

远程执行命令和粘包问题

粘包:

image-20211104162346374

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

1.TCP (transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端〈客户端和服务器端)都要有—一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头〈消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。即面向消息的通信是有消息保护边界的。
3.tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头.

例如客户端在使用recv(1024)方法接受数据时,其最大接受数据为1024个字节,但服务器端发送的数据远远大于这个值,于是客户端的缓存就会有很多数据没拿到,只有1024是真正被网卡取出缓存的,其他数据会在下一次再向客户端请求数据时拿到,第二次很明显无法拿到想要的数据,而是第一次没有读取完的数据。

客户端收数据没收干净,有残留,就会在下一次结果混淆在一起。

解决办法:

每次都将数据收完,不要让其残留在缓存里面。

1.拿到数据的总大小total_size

2.recv_size=0,循环接收,每接收一次,recv_size+=接收的长度

3.直到recv_size=total_size,结束循环

python解决粘包(远程执行命令的代码):

1
2
3
1.服务器端要传入其发送数据的具体字节大小到客户端(也就是头信息)
2.客户端接收数据时要先接收头部信息(字节大小),再根据其字节大小接收具体的数据
#subprocess为远程执行命令
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
#服务器端
#本代码为客户端调用命令远程执行服务器端的命令
import subprocess
from socket import *
import struct
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,client_addr=server.accept()

while True:
try:
res=conn.recv(1024)
if len(res) == 0:break #如果客户端输入的命令为空则推出
obj=subprocess.Popen(res.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
#subprocess.Popen执行命令,stdout和stderr为其正确输出以及错误输出
stdout=obj.stdout.read()
stderr=obj.stderr.read()
total_size=len(stdout)+len(stderr) #统计其输出代码的字节长度
#stdout为命令执行成功输出,stderr为执行错误的输出
header=struct.pack('i',total_size) #将其长度转换为字节bytes,struct.pack 中i的固定大小为4字节,表示其客户端在拿到头部信息时,先收取4字节并解码就可以得到其服务器端传输数据的大小
conn.send(header)
conn.send(stdout+stderr)
except Exception:
break
conn.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#客户端
import struct
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
msg=input('请输入命令').strip()
if len(msg) == 0:continue
client.send(msg.encode('utf-8'))
header=client.recv(4) #拿四个字节的数据大小
total_size=struct.unpack('i',header)[0] #拿到后解码
recv_size=0
cmd_res=b''
while recv_size < total_size: #如果已拿到的数据大小recv_size小于服务器发送过来的总数据大小就继续拿数据。
data=client.recv(1024)
recv_size+=len(data)
cmd_res+=data
print(data.decode('gbk'),end='') #windows编码gbk
else:
print('')

strut转换参数:

image-20211104172547839

解决粘包代码终极版:

(将其封装成一个字典,将字典作为头信息传递过去)

服务器端:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
"""
@作者: egon老湿
@微信:18611453110
@专栏: https://zhuanlan.zhihu.com/c_1189883314197168128
"""
# 服务端应该满足两个特点:
# 1、一直对外提供服务
# 2、并发地服务多个客户端
import subprocess
import struct
import json
from socket import *

server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
server.bind(('127.0.0.1',8083))
server.listen(5)

# 服务端应该做两件事
# 第一件事:循环地从板连接池中取出链接请求与其建立双向链接,拿到链接对象
while True:
conn,client_addr=server.accept()

# 第二件事:拿到链接对象,与其进行通信循环
while True:
try:
cmd=conn.recv(1024)
if len(cmd) == 0:break
obj=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)

stdout_res=obj.stdout.read()
stderr_res=obj.stderr.read()
total_size=len(stdout_res)+len(stderr_res)

# 1、制作头
header_dic={
"filename":"a.txt",
"total_size":total_size,
"md5":"123123xi12ix12"
}

json_str = json.dumps(header_dic)
json_str_bytes = json_str.encode('utf-8')


# 2、先把头的长度发过去
x=struct.pack('i',len(json_str_bytes))
conn.send(x)

# 3、发头信息
conn.send(json_str_bytes)
# 4、再发真实的数据
conn.send(stdout_res)
conn.send(stderr_res)

except Exception:
break
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"""
@作者: egon老湿
@微信:18611453110
@专栏: https://zhuanlan.zhihu.com/c_1189883314197168128
"""
import struct
import json
from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8083))

while True:
cmd=input('请输入命令>>:').strip()
if len(cmd) == 0:continue
client.send(cmd.encode('utf-8'))

# 接收端
# 1、先手4个字节,从中提取接下来要收的头的长度
x=client.recv(4)
header_len=struct.unpack('i',x)[0]

# 2、接收头,并解析
json_str_bytes=client.recv(header_len)
json_str=json_str_bytes.decode('utf-8')
header_dic=json.loads(json_str)
print(header_dic)
total_size=header_dic["total_size"]

# 3、接收真实的数据
recv_size = 0
while recv_size < total_size:
recv_data=client.recv(1024)
recv_size+=len(recv_data)
print(recv_data.decode('utf-8'),end='')
else:
print()


# 粘包问题出现的原因
# 1、tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分
# 2、收数据没收干净,有残留,就会下一次结果混淆在一起

# 解决的核心法门就是:每次都收干净,不要任何残留

服务端为多个客户端提供服务

从上面的代码来说,当一个服务器端处于accept()等待连接的时候,一个客户端来了这个时候代码就会继续往下面走,此时如果再来一个客户端,在请求时就会堵塞住,因为服务器端的主线程已经用于服务第一个客户端去了,无法分身服务多个客户端

解决办法:

1.socketserver

2.多线程(多进程)

这两个模块都能解决单进程服务器端的问题,其实就是服务器端把处理客户端的服务交给其他进程或线程取处理,而自己只负责与客户端连接。类似于nginx,nginx一共两个进程,master和slave,master进程仅负责接收客户端的请求,而真正与客户端通信和返回数据的是slave端。

socketserver:

这里用了socketserver模块,直接省略accpet以及以上监听绑定步骤(服务器端),直接用class类继承socketserver.BaseRequestHandler,然后定义handle函数,在恢复和发送数据时调用self.request.(recv or send)即可。最后生成对象,传入要监听的端口ip以及创建好的类名再运行对象即可。

这里只需要创建对象时传入bind的ip+port,已经通信时调用request方法。极其简单便捷。客户端无需改变,该如何通信还是一样,服务器端除了于客户端通信的那段代码其他均为固定格式

服务器端:

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
import subprocess
import socketserver
import struct
class MyRequestHandle(socketserver.BaseRequestHandler):
def handle(self):
while True:
try:
res=self.request.recv(1024)
if len(res) == 0:break
obj=subprocess.Popen(res.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout=obj.stdout.read()
stderr=obj.stderr.read()
total_size=len(stdout)+len(stderr)
#stdout为命令执行成功输出,stderr为执行错误的输出
header=struct.pack('i',total_size)
self.request.send(header)
self.request.send(stdout+stderr)
except Exception:
break
self.request.close()
s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyRequestHandle)
s.serve_forever()

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import struct
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
msg=input('请输入命令').strip()
if len(msg) == 0:continue
client.send(msg.encode('utf-8'))
header=client.recv(4)
total_size=struct.unpack('i',header)[0]
recv_size=0
cmd_res=b''
while recv_size < total_size:
data=client.recv(1024)
recv_size+=len(data)
cmd_res+=data
print(data.decode('gbk'),end='')
else:
print('')
服务器端代码模板:
1
2
3
4
5
6
7
8
9
10
import subprocess
import socketserver
import struct
class MyRequestHandle(socketserver.BaseRequestHandler):
def handle(self):
while True:
主代码(于客户端通信的代码)
self.request.close() #(关闭套接字代码)
s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyRequestHandle)
s.serve_forever()

多线程实现:

多线程与多进程都可以实现,这里导入多线程模块,把accpet接收到的客户端对象以下代码全部交与线程来做就可以了。(多进程也可以实现,但一般用多线程即可。)这里用threading模块

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import socket
import threading
import struct
import subprocess
# 处理客户端请求的任务
def handle_client_request(ip_port, new_client):
print("客户端的ip和端口号为:", ip_port)
# 5. 接收客户端的数据
# 收发消息都使用返回的这个新的套接字
# 循环接收客户端的消息
while True:
recv_data = new_client.recv(1024)
if recv_data == 'quit':
new_client.close()
break
elif recv_data:
obj = subprocess.Popen(recv_data.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
total_size = len(stdout) + len(stderr)
total_size = struct.pack("i",total_size)
new_client.send(total_size)
new_client.send(stdout)
new_client.send(stderr)
else:
break
if __name__ == '__main__':
# 1. 创建tcp服务端套接字
# AF_INET: ipv4 , AF_INET6: ipv6
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,表示意思: 服务端程序退出端口号立即释放
# 1. SOL_SOCKET: 表示当前套接字
# 2. SO_REUSEADDR: 表示复用端口号的选项
# 3. True: 确定复用
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 2. 绑定端口号
# 第一个参数表示ip地址,一般不用指定,表示本机的任何一个ip即可
# 第二个参数表示端口号
tcp_server_socket.bind(("", 8080))
# 3. 设置监听
# 128: 表示最大等待建立连接的个数
tcp_server_socket.listen(128)
# 4. 等待接受客户端的连接请求
# 注意点: 每次当客户端和服务端建立连接成功都会返回一个新的套接字
# tcp_server_socket只负责等待接收客户端的连接请求,收发消息不使用该套接字
# 循环等待接受客户端的连接请求
while True:
new_client, ip_port = tcp_server_socket.accept()
# 代码执行到此,说明客户端和服务端建立连接成功
# 当客户端和服务端建立连接成功,创建子线程,让子线程专门负责接收客户端的消息
sub_thread = threading.Thread(target=handle_client_request, args=(ip_port, new_client))
# 设置守护主线程,主线程退出子线程直接销毁
sub_thread.setDaemon(True)
# 启动子线程执行对应的任务
sub_thread.start()
# 7. 关闭服务端套接字, 表示服务端以后不再等待接受客户端的连接请求
# tcp_server_socket.close() # 因为服务端的程序需要一直运行,所以关闭服务端套接字的代码可以省略不写

上面为服务端代码,客户端不变,仅为实现服务端多线程

模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import socket
import threading
import struct
import subprocess
def handle_client_request(ip_port, new_client):
while True:
#主代码部分
if __name__ == '__main__':
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
tcp_server_socket.bind(("", 8080))
tcp_server_socket.listen(128)
while True:
new_client, ip_port = tcp_server_socket.accept()
sub_thread = threading.Thread(target=handle_client_request, args=(ip_port, new_client))
sub_thread.setDaemon(True)
sub_thread.start()
# tcp_server_socket.close()

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!