python操作(一):网络编程入门

背景简介

最近做的文本相似度匹配任务,需要部署到web端,毫无网络背景知识的笔者在老板的“压榨”下,3天速成了python websocket通信,并于公司json小哥合作,实现了图片、文本的server-client端通信。

整个要完成的架构如上图所示:Client send一个“字符串/图片”给Server,Server进行一系列运算后,send“字符串/图片”给Client,完成一次通信。在这里我们只记录红块黄块内的工作内容。

网络进程之间如何通信?

TCP/IP协议可以唯一的标识网络中的主机,即“网络层的IP地址”;主机中的应用程序(进程),通过“协议+端口”的形式指定;利用这样的“IP地址+协议+端口”的三元组,就可以标识网络中的唯一一个进程了
而编写/操作TCP/IP协议的应用程序接口,通常采用socket套接字。有一些前辈的直观解释是:socket类似于文件操作的“open—write/read—close”模式,只是将其应用在了TCP/IP协议上

socket的基本操作函数:

socket()函数

int socket(int domain, int type, int protocol)
socket函数对应于一个普通的文件打开操作,返回一个socket描述字,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。通过调用bind()函数给该描述字分配一个端口(PORT),如果不指定会随机分配
domain:地址类型,如:AF_INET决定了要用ipv4地址
type:socket类型
protocol:TCP/IP等传输协议,默认选择跟type类型对应的协议

bind()函数

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器,所以通常服务器端在listen之前会调用bind()

listen()函数

int listen(int sockfd, int backlog);
作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
第一个参数是socket描述字,第二个参数是socket可以排队的最大连接个数

accept()函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
Server端通过socket(),bind(),listen()操作,就可以监听指定的socket地址了。Client端通过requests.get(url)函数建立连接请求,Server端监听到后调用accept()接收这个请求,之后就可以进行两端的I/O操作了。

close()函数

传输完成需要关闭连接。以上是socket的C代码解读,python中的非常类似,如下是用python写的socket传输字符串,基本都可以看懂。
Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import socket
HOST, PORT = '', 8888 # Server端网络IP及端口
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind((HOST, PORT))
listen_socket.listen(1) #设置监听,最多有5个端排队等侯
print 'Serving HTTP on port %s ...' % PORT # 开启了一个侦听socket
while True:
coon, addr = listen_socket.accept() # 开启一个永续循环,被动等待客户端连接
request = coon.recv(1024) # 接收bytes类型数据,每次接收1024b
accept_data = str(request, encoding="utf8")
#print(request)
print("".join(("接收内容:", accept_data, " 客户端口:", str(addr[1]))))

http_response = "Hello World!"

conn.sendall(bytes(http_response, encoding="utf8")) # 重新建立连接后,返回一个服务器中预定义的字符串
coon.close()

Client

1
2
3
4
5
6
7
8
import socket
sk = socket.socket()
sk.connect(("192.168.1.136", 8888)) # 与局域网内Server端IP地址Connect
send_data = "Hello Server"
sk.sendall(bytes(send_data, encoding="utf8"))
accept_data = sk.recv(1024)
print(str(accept_data, encoding="utf8")) # 接收到Server端发来的“Hello World!”
sk.close()

Tips:

  • 需要注意sendrecv函数必须是一一对应的,有接必有收,逻辑上必须足够清楚。否则会造成不知名的一些Error。
  • 在python3中,所有数据的传输必须用bytes类型(bytes只支持ascii码)所以在发送数据的时候要么在发送的字符串前面加 ‘b’,要么使用encode(‘utf-8’)进行转换成bytes类型发送

通过Socket在局域网内传输图片/视频/PDF等

通过上面的工作,已经能够实现普通字符串的传输了,那么怎样搭建一个通用的文件传输系统呢?这里只需要加一个Trick,依然是每次recv(1024),但要循环接收,直到文件全部被传输完毕为止。关键代码如下:
Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import os,socket,hashlib
# ...
# socket监听定义
# ...
while True:
conn, addr = listen_socket.accept()
request = conn.recv(1024)
data = str(request, encoding="utf8")
print("接收内容:", data)
filename = "/home/tucodec/ZJU.mp4"
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")) # send size to client,计算需要循环多少个1024 byte
conn.recv(1024) # 联系两个send会发生粘包,加一个recv
for line in f:
m.update(line)
conn.send(line) # 循环发送
f.close()
print("send done...")

Client

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
import socket,hashlib
while True:
sk = socket.socket()
sk.connect(("192.168.1.136", 8888)) # 主动初始化与服务器端的连接
while True:
strs = input("请输入:")
if len(strs) == 0:
continue
sk.send(bytes(strs, encoding="utf8"))
server_resp_size = sk.recv(1024) # 接收send_filesize,bytes类型的数据
file_total_size = int(server_resp_size.decode()) # 格式转换
sk.send(bytes("开始接收文件...", encoding="utf8"))
f = open('new.mp4', "wb")
m = hashlib.md5()
recv_size = 0
while recv_size < file_total_size:
if file_total_size - recv_size > 1024: #最后一次接收之前,接收大小设置为1024
size = 1024
else: #最后一次不足1024,则只接收文件剩余的部分
size = file_total_size - recv_size
data = sk.recv(size)
recv_size += len(data)
f.write(data)
m.update(data)
f.close()
sk.close()
break

aaugustin/websockets

尝试了socket的细节后,对网络编程有了些基础的认识。但是要和json小哥通信还是不行的,用上述的socket.recv(1024)方法得到的,是小哥发过来的是Http报头,需要各种校验,才能完成握手。果断上Github寻找别人写好的包,这时aaugustin/websockets以其简洁管事儿进入了我的视线,直接把文档中的例子拿来跑便可以一目了然。
Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python

import asyncio
import websockets

async def hello(websocket, path):
name = await websocket.recv()
print("< {}".format(name))

greeting = "Hello {}!".format(name)
await websocket.send(greeting)
print("> {}".format(greeting))

start_server = websockets.serve(hello, '', 8888)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python

import asyncio
import websockets

async def hello():
async with websockets.connect('ws://192.168.1.136:8888') as websocket:
name = input("What's your name? ")
await websocket.send(name)
print("> {}".format(name))

greeting = await websocket.recv()
print("< {}".format(greeting))

asyncio.get_event_loop().run_until_complete(hello())

查看某个网络端口占用并将其Kill

1
2
$ lsof -i :8888
$ kill 4688

Refer:

aaugustin/websockets
Python HTTP 库:requests 快速入门
使用Python来编写HTTP服务器的超级指南
网络编程学习笔记一:Socket编程
Python3 Socket网络编程
Python 3.x–Socket实现简单的ssh和文件下载功能