3.1.2 Socket网络通信开发
Socket语法
Python中,我们用Socket()函数来创建套接字,语法如下:
socket.socket([family[, type[, proto]]])
参数
- family:套接字家族可以使用AF_UNIX或者AF_INET
- type:套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM
- protocol:一般不填默认为0
Socket对象方法
| 函数 | 描述 |
| 服务器端套接字 | |
| s.bind() | 绑定地址(host, port)到套接字, 在AF_INET下,以元组(host, port)的形式表示地址 |
| s.listen() | 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了 |
| s.accept() | 被动接受TCP客户端连接,(阻塞式)等待连接的到来 |
| 客户端套接字 | |
| s.connect() | 主动初始化TCP服务器连接。一般address的格式为元组(hostname, port),如果连接出错,返回socket.error错误 |
| s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
| 公用套接字 | |
| s.recv() | 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略 |
| s.send() | 发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小 |
| s.sendall() | 完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常 |
| s.recvfrom() | 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址 |
| s.sendto() | 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr, port)的元组,指定远程地址。返回值是发送的字节数 |
| s.close() | 关闭套接字 |
| s.getpeername() | 返回连接套接字的远程地址。返回值通常是元组(ipaddr, port) |
| s.getsockname() | 返回套接字自己的地址。通常是一个元组(ipaddr, port) |
| s.setsockopt(level, optname, value) | 设置给定套接字选项的值 |
| s.getsockopt(level, optname[bullen]) | 返回套接字选项的值 |
| s.settimeout(timeout) | 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect()) |
| s.gettimeout() | 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None |
| s.fileno | 返回套接字的文件描述符 |
| s.setblocking(flag) | 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常 |
| s.makefile() | 创建一个与该套接字相关连的文件 |
Socket简单实例
服务器端
import socket
server = socket.socket() #生成一个socket实例对象
HOST = socket.gethostname() #获取主机名
PORT = 9999
server.bind((HOST, PORT)) #绑定主机名和端口
server.listen(5) #监听
print('开始监听...')
conn, addr = server.accept() #等待连接,conn就是客户端连接过来在服务器端生成的连接实例
print(conn, addr)
data = conn.recv(1024) #设定接收数据大小,单位为字节
print('recv: ', data)
conn.send(data.upper()) #将接收到的数据大写后再发回
server.close()
客户端
import socket
client = socket.socket() #生成一个套接字对象
HOST = socket.gethostname() #获取本机主机名
PORT = 9999
client.connect((HOST, PORT))
client.send(b'Hello World!') #发送数据,在python3.x中,只能发送bytes类型的数据
data = client.recv(1024) #设定接收数据的大小,单位是字节
print('recv: ', data)
client.close()
服务器端输出
开始监听...
<socket.socket fd=568, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.1.3', 9999), raddr=('192.168.1.3', 57354)> ('192.168.1.3', 57354)
recv: b'Hello World!'
客户端输出
recv: b'HELLO WORLD!'
但上面的示例有很多问题。
首先,它是写死的,每运行一次只能发送一条消息,并且消息也是固定的。
其次,它同时只能跟一个客户端进行通信,在实际生产中,这种程序基本没用。
我们先来解决第一个问题,改进上面的示例,让它可以多次收发数据。
服务器端
import socket
server = socket.socket() #生成一个socket实例对象
HOST = socket.gethostname() #获取主机名
PORT = 9999
server.bind((HOST, PORT)) #绑定主机名和端口
server.listen(5) #监听
print('开始监听...')
while True: #此循环可以让服务器端可以不断的等待接收新链接
conn, addr = server.accept() #等待连接,conn就是客户端连接过来在服务器端生成的连接实例
print(conn, addr)
while True: #此循环让服务器端可以不断的接收新数据
data = conn.recv(1024) #设定接收数据大小,单位为字节
print('recv: ', data)
if not data:
print('client has lost...')
break
conn.send(data.upper()) #将接收到的数据大写后再发回
客户端
import socket
client = socket.socket() #生成一个套接字对象
HOST = socket.gethostname() #获取本机主机名
PORT = 9999
client.connect((HOST, PORT))
while True: #加一个循环,让客户端可以不断的发送数据
msg = input('>>> ').strip()
if len(msg) == 0: #不加这个判断,发送数据为空,程序会卡住
continue
client.send(msg.encode('utf-8')) #发送数据,在python3.x中,只能发送bytes类型的数据
data = client.recv(1024) #设定接收数据的大小,单位是字节
print('recv: ', data)
此时该示例程序不仅可以实现单个链接反复收发消息,还允许多个链接挂起等待。需在windows命令行下实现,在pycharm中当结束第一个客户端链接时,客户端和服务器端会同时报错,但在cmd命令行中则不会。
步骤1
如下图所示,启动服务器端后,挂起两个链接,可以看到,第一个链接已经接入。
链接一开始和服务器端互动,互动结束按ctrl+c,链接2自动接入并等待收发信息
链接2与服务器端互动
要想实现服务器同时与多个客户端同时连接并持续收发消息,须掌握异步相关知识。目前只能做到同时与一个客户端连接并持续收发消息。
上面的示例还有个问题,关于recv()接收大小的问题。比如下面这个简单的ssh示例:
收发大数据
先看下面的示例
服务器端
import socket
import os
server = socket.socket()
host = socket.gethostname()
port = 9999
server.bind((host, port))
print(host)
server.listen(5)
while True:
conn, addr = server.accept()
print('new conn: ', addr)
while True:
print('waiting new orders...')
data = conn.recv(1024)
if not data:
print('客户端已断开')
break
print('执行命令: ', data)
cmd_res = os.popen(data.decode()).read() #2接收到数据后解码
print('before send', len(cmd_res))
if len(cmd_res) == 0:
cmd_res = 'cmd has no output...'
conn.send(cmd_res.encode('utf-8')) #3再发回时依然需要编码
print('send done!')
server.close()
客户端
import socket
client = socket.socket()
host = socket.gethostname()
port = 9999
client.connect((host, port))
while True:
cmd = input('\>: ').strip()
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8')) #1未编码的数据为str类型,无法发送
cmd_res = client.recv(1024)
print(cmd_res.decode()) #4再解码得到结果
client.close()
运行结果如下
上面2个窗口分别是server端和client端,并在client端输出tasklist命令结果。第三个窗口是操作系统直接输出tasklist命令的结果。我们可以看到2个结果对比,明显不一样。client端的结果只显示了10来条就结束了,并且收到的最后一条也不完整。为什么会出现这个问题?
答案在于recv()函数,recv()函数接收到的数值定义了接收值的大小。程序中设定的是1024,即1024字节,也就是我们常说的1kb。上面这个程序在运行dir等输出结果小于1024字节的命令时不会出错,但当输出结果大于1024时,系统会将大于1024字节的部分暂存在系统的缓冲区里,在下一次客户端与服务器端交互时释放。
很明显,这不是我们要的效果。我们要的是输入一个命令,它能够一次返回所有的输出结果。那么,我们要如何解决这个问题呢?
答案就是在发送前服务器计算要发送的数据长度,将这个值发送到客户端,开始循环接收,只要接收长度与数据实际长度不相等就一直接收。然后结束。
服务器端
import socket
import os
server = socket.socket()
host = socket.gethostname()
port = 9999
server.bind((host, port))
print(host)
server.listen(5)
while True:
conn, addr = server.accept()
print('new conn: ', addr)
while True:
print('waiting new orders...')
data = conn.recv(1024)
if not data:
print('客户端已断开')
break
print('执行命令: ', data)
cmd_res = os.popen(data.decode()).read() #2接收到数据后解码
print('before send', len(cmd_res))
if len(cmd_res) == 0:
cmd_res = 'cmd has no output...'
conn.send(str(len(cmd_res.encode('utf-8'))).encode('utf-8'))
#第一个encode(): 数据长度必须转码,否则客户端接收长度与数据实际长度不一致
#第二个encode(): len数据类型无法转码,必须先转换成str类型
conn.send(cmd_res.encode('utf-8')) #3再发回时依然需要编码
print('send done!')
server.close()
客户端
import socket
client = socket.socket()
host = socket.gethostname()
port = 9999
client.connect((host, port))
while True:
cmd = input('\>: ').strip()
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8')) #1未编码的数据为str类型,无法发送
cmd_size = client.recv(1024) #接收命令结果的长度
print('length of cmd_res: ', cmd_size)
received_size = 0
while received_size != int(cmd_size.decode()): #只要接收值和数据值不相等就一直收
data = client.recv(1024)
received_size += len(data) #计算实际接收长度
print(received_size)
print(data.decode()) #4再解码得到结果
else:
print('Transmission has been done!', received_size)
client.close()
运行结果
可以看到实际接收长度与数据长度一致后,程序自动进入下一个命令等待中。
怎么样,看着很完美了是吧。但以上程序还有个小问题,服务器端里有两行代码连续使用了send()。
conn.send(str(len(cmd_res.encode('utf-8'))).encode('utf-8'))
#第一个encode(): 数据长度必须转码,否则客户端接收长度与数据实际长度不一致
#第二个encode(): len数据类型无法转码,必须先转换成str类型
conn.send(cmd_res.encode('utf-8')) #3再发回时依然需要编码
这里有可能会造成粘(nian2)包问题。2次数据合并成一次发送。如何解决呢?
服务器端
import socket
import os
server = socket.socket()
host = socket.gethostname()
port = 9999
server.bind((host, port))
print(host)
server.listen(5)
while True:
conn, addr = server.accept()
print('new conn: ', addr)
while True:
print('waiting new orders...')
data = conn.recv(1024)
if not data:
print('客户端已断开')
break
print('执行命令: ', data)
cmd_res = os.popen(data.decode()).read() #2接收到数据后解码
print('before send', len(cmd_res))
if len(cmd_res) == 0:
cmd_res = 'cmd has no output...'
conn.send(str(len(cmd_res.encode('utf-8'))).encode('utf-8'))
#第一个encode(): 数据长度必须转码,否则客户端接收长度与数据实际长度不一致
#第二个encode(): len数据类型无法转码,必须先转换成str类型
client_ack = conn.recv(1024)
print('ack from client', client_ack)
#添加这两行让服务器端和客户端多一次交互以解决粘包问题
conn.send(cmd_res.encode('utf-8')) #3再发回时依然需要编码
print('send done!')
server.close()
客户端
import socket
client = socket.socket()
host = socket.gethostname()
port = 9999
client.connect((host, port))
while True:
cmd = input('\>: ').strip()
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8')) #1未编码的数据为str类型,无法发送
cmd_size = client.recv(1024) #接收命令结果的长度
print('length of cmd_res: ', cmd_size)
client.send('ready to transmit'.encode('utf-8')) #增加一次交互以解决粘包
received_size = 0
received_data = b'' #定义一个空数据
while received_size != int(cmd_size.decode()): #只要接收值和数据值不相等就一直收
data = client.recv(1024)
received_size += len(data) #计算实际接收长度
# print(received_size)
# print(data.decode()) #4再解码得到结果
received_data += data #每次循环更新接收数据
else:
print('Transmission has been done!', received_size)
print(received_data.decode()) #循环完毕输出数据
client.close()
运行结果就不贴了,因为粘包问题不是每次都会发生。但解决的方法务必要掌握。
到这里已经掌握了如何连续收发数据。
基于以上的内容,下面实现一个FTP,通常FTP分以下几个步骤:
- 读取文件名
- 检测文件名是否存在
- 打开文件
- 检测文件长度
- 发送长度给客户端
- 等待客户端确认(这一步是为了防止粘包)
- 开始边读边发送数据以及md5
- 传送完毕后与客户端接收到的最后的md5进行比对
下面是一个简单的FTP示例:
服务器端
import socket
import os
import hashlib
host = socket.gethostname()
port = 9999
server = socket.socket()
server.bind((host, port))
server.listen(5)
while True:
conn, addr = server.accept() #等待连接
print('From Server: Connection established.', addr)
while True:
print('Waiting for new orders....')
data = conn.recv(1024) #接收命令
if not data:
print('From Server: Connection has failed.')
break
cmd, filename = data.decode().split()
print('From Server: Filename is ', filename)
if os.path.isfile(filename):
f = open(filename, 'rb')
m = hashlib.md5()
file_size = os.stat(filename).st_size
conn.send(str(file_size).encode('utf-8'))
#发送文件长度,file_size是int类型,无法直接编码,须先转成str类型
conn.recv(1024) #等待客户端确认
for line in f:
m.update(line) #逐行更新md5值
conn.send(line) #开始逐行发送数据
print('From Server: MD5: ', m.hexdigest()) #文件发送完毕,md5更新完毕
f.close()
conn.send(m.hexdigest().encode())
print('From Server: Transmission has been done!')
客户端
import socket
import hashlib
host = socket.gethostname()
port = 9999
client = socket.socket()
client.connect((host, port)) #发起连接
while True:
cmd = input('\>:').strip()
if len(cmd) == 0:
continue
if cmd.startswith('get'):
client.send(cmd.encode('utf-8')) #发送传送文件的命令
server_response = client.recv(1024) #接收文件长度
print('From Client: Server response: ', server_response)
client.send(b'Ready to transmit.') #发送确认信息
file_size = int(server_response.decode())
#接收到的文件长度需要先解码然后转换成int类型
received_size = 0
filename = cmd.split()[1]
f = open(filename + '_new', 'wb')
m = hashlib.md5()
while received_size < file_size:
if file_size - received_size > 0: #意为不止收一次
size = 1024
else: #剩多少收多少
size = file_size - received_size
print('last receive: ', size)
data = client.recv(size) #每次接收的长度
received_size += len(data) #已收到长度随循环自增
m.update(data) #不断更新md5
f.write(data) #不断写入
#print('From Client: File size: %d, Received size: %d' % (file_size, received_size))
else:
new_file_md5 = m.hexdigest() #传送完毕后的md5
print('From Client: Transmission has been done!')
f.close()
server_md5 = client.recv(1024)
print('From Client: Client MD5: ', new_file_md5)
print('From Server: Server MD5: ', server_md5)
输出结果:
服务器端
From Server: Connection established. ('192.168.1.3', 53230)
Waiting for new orders....
From Server: Filename is uplayinstaller.exe
From Server: MD5: 955765b2cba4489a3639758aecab94a3
From Server: Transmission has been done!
Waiting for new orders....
客户端
\>:get uplayinstaller.exe From Client: Server response: b'103203256' From Client: Transmission has been done! From Client: Client MD5: 955765b2cba4489a3639758aecab94a3 From Server: Server MD5: b'955765b2cba4489a3639758aecab94a3' \>:
这个程序已经能实现简单的文件传输,但依然有很多不足的地方。比如,程序不能处理错误,传输文件只能在程序所在文件夹进行。比较成熟的FTP项目会在后面进行。
关于Socket的内容就写到这。





浙公网安备 33010602011771号