基本示例
两个节点想要通过网络进行通信,目前最常见的方式就是通过socket来进行。简单来说,大致流程如下:
服务器端
- 开启一个socket,并绑定到指定端口号上
- 在端口监听,等待连接
- 当有连接请求达到时,接受请求建立连接
- 开始接收消息
- 接收成功后给客户端返回提示
- 关闭端口
客户端
- 开启一个socket
- 去连接远程服务器端口上的socket
- 发送消息
- 若发送成功则会接受到服务器的提示信息
- 关闭端口
在Python中的一个简单实现如下,注意在python中socket只能收发byte型数据,所以需要对字符串进行转码。
服务器端:
import socket
server=socket.socket() #开启一个socket端口
server.bind(('localhost',1111)) #绑定到指定地址
server.listen() #开始监听
conn,addr=server.accept() #接受连接
data=conn.recv(1024) #缓冲区大小
print(data.decode())
conn.send("recived!\n".encode("utf-8")) #给发送方返回数据
server.close()
客户端:
import socket
client=socket.socket() #开启一个socket端口
client.connect(('localhost',1111)) #去连接指定地址
client.send("message\n".encode("utf-8")) #socket只能收发字节型数据
data=client.recv(1024) #此处的data为接受返回值用的
print(data.decode())
client.close()
服务器端与客户端的运行结果分别为:
message recived!
在上述代码中,如果客户端断开连接(Ctrl+C)则服务器的程序也会因连接出错而结束,想要服务器端一直保持监听状态,改进后的代码如下。
服务器端:
import socket
server=socket.socket() #开启一个socket端口
server.bind(('localhost',1111)) #绑定到指定地址
server.listen() #开始监听
while True:
conn,addr=server.accept() #接受连接
while True:
msg=conn.recv(1024) #缓冲区大小
if not msg: #判断连接丢失
print("connection lost...")
break
print(msg.decode())
conn.send(msg) #给发送方返回数据
server.close()
客户端:
import socket
client=socket.socket() #开启一个socket端口
client.connect(('localhost',1111)) #去连接指定地址
while True:
msg=input(">>:").strip()
if len(msg)==0:
continue
client.send(msg.encode("utf-8")) #socket只能收发字节型数据
data=client.recv(1024) #此处的data为接受返回值用的
print(data.decode())
client.close()
开启三个会话窗口,分别模拟服务器、客户端1、客户端2,易得客户端2会被阻塞,因为唯一的连接被客户端1占用了:
此时强行将客户端1断开,之前被阻塞的客户端2会与服务器建立连接并把消息发送过去:
模拟SSH远程执行命令
使用os模块中的popen(),简单修改一下客户端代码即可模仿SSH。
num=1
assert type(num) is int
import socket
import os
server=socket.socket() #开启一个socket端口
server.bind(('localhost',1111)) #绑定到指定地址
server.listen() #开始监听
while True:
conn,addr=server.accept() #接受连接
while True:
msg=conn.recv(1024) #缓冲区大小,对于某些指令的长返回结果,会限制返回数据的大小
if not msg: #判断连接丢失
print("connection lost...")
break
res=os.popen(msg.decode()).read()
if len(res)==0:
res="empty output!" #不能返回空结果,会导致客户端卡住
conn.send(res.encode("utf-8")) #返回执行结果
server.close()
但是这里有一个问题,因为缓冲区是在建立连接时就商定好的,对于有些可能返回长结果的指令,如ifconfig,或是在有很多文件(夹)的路径执行ls,无法一次性将指令的执行结果发送给客户端。更改缓冲区大小显然是不可取的,可行的办法是多次发送,具体代码如下(特别注意格式的转换,socket只能发送byte型数据,在发送前与接收后记得处理)。
服务器:
import socket
import os
server=socket.socket() #开启一个socket端口
server.bind(('localhost',1111)) #绑定到指定地址
server.listen() #开始监听
while True:
conn,addr=server.accept() #接受连接
while True:
msg=conn.recv(1024) #缓冲区大小
if not msg: #判断连接丢失
print("connection lost...")
break
res=os.popen(msg.decode()).read() #获取执行结果
res_size=len(res.encode("utf-8")) #计算执行结果的字节型长度
if res_size==0:
res="empty output!"
# 将数据长度发送过去以供客户端准备接收的次数
conn.send(str(res_size).encode("utf-8"))
conn.send(res.encode("utf-8")) #发送正式数据
server.close()
客户端:
import socket
client=socket.socket() #开启一个socket端口
client.connect(('localhost',1111)) #去连接指定地址
while True:
msg=input(">>:").strip()
if len(msg)==0:
continue
client.send(msg.encode("utf-8")) #发送指令
res_size=int(client.recv(1024).decode()) #得到数据总长度
recived_size=0
while recived_size<res_size:
res_part=client.recv(1024) #接受部分数据
recived_size+=len(res_part) #计算接收长度
print(res_part.decode())
client.close()
在windows平台启动两者,在客户端输入ipconfig,代码的输出与CMD的输出一致:
但是如果在Linux上运行,则会出现这样的错误:
原因在于Linux上的服务器端将数据的大小与数据首部分同时发给了客户端,这就导致了客户端在获取数据大小时对含有数据的信息整形化导致的,换句话说,数据的大小与数据本身发生了粘结,即粘包现象。有一种简单的办法是在发送数据大小与发送数据之间休眠短暂的时间,但这会严重影响程序的实时性,有一种巧妙的解决办法是受TCP协议启发,在发送了数据大小之后、正式发送数据之前,让客户端与服务器再进行一次交互,这次交互可以是无意义的,也可以用于数据大小的二次校验。修改后的代码如下。
服务端:
import socket
import os
server=socket.socket() #开启一个socket端口
server.bind(('localhost',1111)) #绑定到指定地址
server.listen() #开始监听
while True:
conn,addr=server.accept() #接受连接
while True:
msg=conn.recv(1024) #缓冲区大小
if not msg: #判断连接丢失
print("connection lost...")
break
res=os.popen(msg.decode()).read() #获取执行结果
res_size=len(res.encode("utf-8")) #计算执行结果的字节型长度
if res_size==0:
res="empty output!"
# 将数据长度发送过去以供客户端准备接收的次数
conn.send(str(res_size).encode("utf-8"))
ack=conn.recv(1024) #接受客户端返回的应答
if int(ack.decode())==res_size: #校验无误
conn.send(res.encode("utf-8")) # 发送正式数据
server.close()
客户端:
import socket
client=socket.socket() #开启一个socket端口
client.connect(('localhost',1111)) #去连接指定地址
while True:
msg=input(">>:").strip()
if len(msg)==0:
continue
client.send(msg.encode("utf-8")) #发送指令
res_size=int(client.recv(1024).decode()) #得到数据总长度
client.send(str(res_size).encode("utf-8")) #返回接受的长度以供校验并避免粘包
recived_size=0
while recived_size<res_size:
res_part=client.recv(1024) #接受部分数据
recived_size+=len(res_part) #计算接收长度
print(res_part.decode())
client.close()
简易FTP
使用Python中的socket实现一个简单的FTP原型,规定指令格式为”get file_path”。
服务端与客户端在交互上首先需要满足前文的几点要求:
- 服务端不能返回空内容,这会导致客户端一直等待
- 客户端不能发送空内容,空内容在客户端被无视,并且若服务端接收到空内容则视为客户端掉线
- 在正式发送/接受数据之前增加一次会话,避免粘包现象
服务端:
import socket,os
server=socket.socket() #开启一个socket端口
server.bind(('localhost',1111)) #绑定到指定地址
server.listen() #开始监听
while True:
conn,addr=server.accept() #接受连接
while True:
cmd=conn.recv(1024).decode() #缓冲区大小
if not cmd: #判断连接丢失
print("connection lost...")
break
if cmd.split(" ")[0]=="get": #检查指令正确性
file_path = cmd.split(" ")[1]
if os.path.isfile(file_path): #检查路径正确性
fd=open(file_path,"rb")
file_size=os.stat(file_path).st_size #获取文件大小
if file_size == 0:
print("empty file!")
continue
# 将文件尺寸发送过去以供客户端准备接收的次数
conn.send(str(file_size).encode("utf-8"))
ack = conn.recv(1024) # 接受客户端返回的应答
if int(ack.decode()) == file_size: # 校验无误
for line in fd:
conn.send(line)
fd.close()
print("send compelete!")
else:
print("file_path err!")
else:
print("cmd err!")
continue
server.close()
客户端:
import socket
client=socket.socket() #开启一个socket端口
client.connect(('localhost',1111)) #去连接指定地址
while True:
msg=input(">>:").strip() #规定获取文件的指令格式为"get file_path"
if len(msg)==0: #无视空输入
continue
file_path = msg.split(" ")[-1] #提取文件路径
client.send(msg.encode("utf-8")) #发送指令
file_size=int(client.recv(1024).decode()) #得到数据总长度
client.send(str(file_size).encode("utf-8")) #返回接受的长度以供校验并避免粘包
recived_size=0 #已接收的文件长度
fd=open(file_path+".bak","wb")
while recived_size<file_size:
res_part=client.recv(1024) #接受部分数据
fd.write(res_part)
recived_size+=len(res_part) #累加接收长度
fd.close()
print("recive compelete!")
client.close()
socket server
以上实现的服务端均是单线程的,只支持连接一个客户端。Python中提供了原生支持多线程的socket库,那就是socketserver,官网给出的基本示例如下。
服务端:
import socketserver
# 请求处理类,每次连接都会进行一次实例化
class MyTCPHandler(socketserver.BaseRequestHandler):
# handle()为必须重写的方法,实现与客户端的通信
def handle(self):
#每次实例化(即接收连接)时显示客户端的地址
print("接受到来自{}的连接".format(self.client_address[0]))
while True:
try:
# self.request是与客户端相连的socket
self.data = self.request.recv(1024).strip()
#显示客户端发送的内容
print("{}".format(self.data.decode()))
#返回原内容
self.request.send(self.data)
#捕获连接中断异常
except ConnectionResetError as e:
print(e)
break #此时Handler实例会自动释放
if __name__ == "__main__":
HOST, PORT = "localhost", 1111
#创建线程级TCP服务端,支持多并发
server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
#服务持久化
server.serve_forever()
客户端:
import socket
client=socket.socket()
client.connect(("localhost",1111))
while True:
msg=input(">>:").strip()
if len(msg)==0:
continue #无视空输入
client.send(msg.encode("utf-8"))
data=client.recv(1024)
print("recv:{}".format(data.decode()))
client.close()