Python Socket网络通信

互联网协议

按照功能的不同分为七层。

OSI七层协议:应,表,会,传,网,数,物。

其中应表会可以归为应用层

  • 应用层:应用程序。http,ftp协议
  • 传输层:tcp/udp协议。端口来标识运行中的一款应用程序。 以太网头|IP头|tcp头|数据
  • 网络层:IP协议,用于寻找子网。
  • 数据链路层:Ethernet(以太网)协议,将数据按标准分组。
  • 物理层:物理设备,发射电信号,如:101001

tcp也称作流式协议,需要双向管道(来回)。

通过握手确认建立临时管道。

udp协议不需要通道,不会等待对方确认是否收到。

效率比tcp协议高,但是不可靠。(只负责丢)

socket层

在应用层和传输层(tcp/udp)之间的中间软件抽象层,它是一组接口。

socket为套接字编程。

一个简单的服务端和客户端

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
iimport socket

# 创建一个套间字对象。 (socket.AF_INET 套间字类型 socket.SOCK_STREAM 是tcp传输协议)
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定一个端口。 port: 0-65535 其中0-1024是给操作系统使用
phone.bind(('127.0.0.1', 8080))

# 开始监听。 参数为最大挂起的连接数
phone.listen(5)

# 服务端会在这里进行等待连接接入,直到有连接才会进行下一步
conn, client_address = phone.accept()

# 接收消息,限制为:1024 bytes
data = conn.recv(1024)
print(data)

# 回复信息给客户端
conn.send(data.upper())

# 关闭连接和关闭服务
conn.close()
phone.close()

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
iimport socket

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

phone.connect(('127.0.0.1', 8080))

# 发送数据到服务端时,只能发送二进制编码字符(这里用encode将字符串转换为二进制类型)
phone.send('hello'.encode('utf-8'))

# 从服务端接受回信息
data = phone.recv(1024)
print(data)
phone.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
import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 快速回收端口
phone.bind(('127.0.0.1', 8080))

phone.listen(5)
print('staring...')

while True: # 连接循环
conn, client_address = phone.accept()

while True: # 通信循环
try:
data = conn.recv(1024)
# 适用于linux系统,客户端断开连接
if not data:
break
print(data)
conn.send(data.upper())
except ConnectionResetError: # 适用于windows系统,客户端断开连接
break
conn.close()

phone.close()

客户端

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

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

phone.connect(('127.0.0.1', 8080))

while True:
msg = input('>> ').strip()
if not msg:
continue
phone.send(msg.encode('utf-8'))
data = phone.recv(1024)
print(data.decode('utf-8'))

phone.close()

实现类似ssh的效果(远程执行命令)

服务端

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
import subprocess

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8080))

phone.listen(5)
print('staring...')

while True: # 连接循环
conn, client_address = phone.accept()

while True: # 通信循环
try:
cmd = conn.recv(1024)
# 适用于linux系统,客户端断开连接
if not cmd:
break
# 执行命令
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stdout.read()
conn.send(stdout+stderr)

except ConnectionResetError: # 适用于windows系统,客户端断开连接
break
conn.close()

phone.close()

客户端

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

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

phone.connect(('127.0.0.1', 8080))

while True:
cmd = input('>> ').strip()
if not cmd:
continue
phone.send(cmd.encode('utf-8'))
data = phone.recv(1024)
print(data.decode('utf-8'))

phone.close()

这里会留下一个问题,每次接受限制了1024个字节,会产生了粘包现象。

粘包现象

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

底层原理

1.不管是recv还是send都不是直接接受对方的消息,而是操作自己的操作系统。

(所以recv没法超过自己内存的大小)

2.tcp协议会对间隔短的多个包进行优化合并

3.recv和send方法,可以一直调用,直到内存里的值被取完

解决粘包现象

一种方法:先把数据的长度发送给客户端,再发送真实数据。

服务端

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
import socket
import struct
import subprocess

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8080))

phone.listen(5)
print('staring...')

while True: # 连接循环
conn, client_address = phone.accept()

while True: # 通信循环
try:
cmd = conn.recv(1024)
if not cmd:
break
# 执行命令
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stdout.read()

# 计算结果的总字节数
total_size = len(stdout) + len(stderr)
# 使用struct打包结果,返回的值永远为4字节
header = struct.pack('i', total_size)

# 先发送报头,即数量的字节大小
conn.send(header)

# 再发送数据
conn.send(stdout)
conn.send(stderr)

except ConnectionResetError:
break
conn.close()

phone.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
import socket
import struct

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

phone.connect(('127.0.0.1', 8080))

while True:
cmd = input('>> ').strip()
if not cmd:
continue
phone.send(cmd.encode('utf-8'))

# 先接收数据大小,永远返回4个字节
header = phone.recv(4)
total_size = struct.unpack('i', header)[0]

recv_size = 0
recv_data = b''
while recv_size < total_size:
res = phone.recv(1024)
recv_data += res
recv_size += len(res)

print(recv_data.decode('utf-8'))

phone.close()

上述的方法有个弊端是,pack方法打包不能超过2147483647

1
2
3
# 可以使用长整数类型,不过也是有限制的
res = struct.pack('l', 10000000000000)
print(res, len(res)) # 返回8字节

另一个解决办法:

1.先发送报头长度

2.再发送报头的数据

3.最后发送真实数据

服务端

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
import json
import socket
import struct
import subprocess

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8080))

phone.listen(5)
print('staring...')

while True: # 连接循环
conn, client_address = phone.accept()

while True: # 通信循环
try:
cmd = conn.recv(1024)
if not cmd:
break
# 执行命令
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stdout.read()

# 把所有结果打包成字典
header_dic = {
'filename': 'test.txt',
'size': len(stdout) + len(stderr)
}
# 再将字典序列化,然后打包为报头
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')

# 先发送报头长度
conn.send(struct.pack('i', len(header_bytes)))

# 再发送报头
conn.send(header_bytes)

# 最后发送数据
conn.send(stdout)
conn.send(stderr)

except ConnectionResetError:
break
conn.close()

phone.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
import json
import socket
import struct

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

phone.connect(('127.0.0.1', 8080))

while True:
cmd = input('>> ').strip()
if not cmd:
continue
phone.send(cmd.encode('utf-8'))

# 先接收报头长度
obj = phone.recv(4)
header_size = struct.unpack('i', obj)[0]

# 再接收报头
header_bytes = phone.recv(header_size)

# 解析需要的真实数据
header_json = header_bytes.decode('utf-8')
header_dic = json.loads(header_json)
print(header_dic)
total_size = header_dic['size']

recv_size = 0
recv_data = b''
while recv_size < total_size:
res = phone.recv(1024)
recv_data += res
recv_size += len(res)

print(recv_data.decode('utf-8'))

phone.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
45
46
47
48
49
50
51
import json
import os
import socket
import struct
import subprocess

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8080))

phone.listen(5)
print('staring...')

while True: # 连接循环
conn, client_address = phone.accept()

while True: # 通信循环
try:
res = conn.recv(1024) # 比如这里使用的是 get a.txt 命令
if not res:
break

cmds = res.decode('utf-8').split()
filename = cmds[1]

# 把所需数据写成字典
header_dic = {
'filename': filename,
'size': os.path.getsize(filename)
}

# 再将字典序列化,然后打包为报头
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')

# 先发送报头长度
conn.send(struct.pack('i', len(header_bytes)))

# 再发送报头
conn.send(header_bytes)

# 最后发送文件数据
with open(filename, 'rb') as f:
for line in f:
conn.send(line)

except ConnectionResetError:
break
conn.close()

phone.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
import json
import socket
import struct

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

phone.connect(('127.0.0.1', 8080))

while True:
cmd = input('>> ').strip()
if not cmd:
continue
phone.send(cmd.encode('utf-8'))

# 先接收报头长度
obj = phone.recv(4)
header_size = struct.unpack('i', obj)[0]

# 再接收报头
header_bytes = phone.recv(header_size)

# 解析需要的真实数据
header_json = header_bytes.decode('utf-8')
header_dic = json.loads(header_json)
print(header_dic)
total_size = header_dic['size']
filename = header_dic['filename']

# 写入数据
with open(filename + '_download', 'wb') as f:
recv_size = 0
while recv_size < total_size:
line = phone.recv(1024)
f.write(line)
print('{0}/{1}'.format(recv_size, total_size))
recv_size += len(line)

phone.close()

udp协议实现

比如查询操作就适合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, client_addr = server.recvfrom(1024)
server.sendto(data.upper(), client_addr)

客户端

1
2
3
4
5
6
7
8
9
10
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
msg = input('>>: ').strip()
client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))

data, server_addr = client.recvfrom(1024)
print(data, server_addr)