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()
服务器端输出
开始监听...('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的内容就写到这。