前言
在网络编程里总会涉及到socket编程,或者说,网络编程是基于socket之上的。通过socket,我们可以建立tcp连接,或是udp通讯方式。亏得Python的完美封装,Socket编程变得容易上手。
接下来会写一个基于tcp方式的简易终端聊天系统。
实例一个socket
在Python中创建一个socket对象需要导入socket包,还需要指定协议。
import socket
socketObj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- AF_INET 表示使用ipv4协议;
- SOCK_STREAM 表示连接基于tcp,如果想用udp,则为SOCK_DGRAM。
此时socketObj是程序中的关键。关于它的常用方法有以下:
- bind() 绑定地址(ip和端口),接收参数为元组类型。如:
socketObj.bind(("0.0.0.0", 8080))
; - listen() 设置监听,只有服务器端需要设置。接收参数为int类型。
socketObj.listen(10)
表示最多接收10个客户端的连接; - accept() 等待连接,只有服务器端需要设置。返回一个元组,包含了与客户端连接的socket和客户端的地址信息(这个地址信息也是个元组,与bind()函数接收的参数格式一样);
- connect() 用于客户端连接服务器,参数为服务器的地址信息。如:
client.connect(("0.0.0.0", 8080))
; - recv() 接收数据。接收参数为int型,表示一次接收数据的字节长度;返回接收到的数据,此时data为byte型。存在recvfrom()方法,不但返回接收到的数据信息,还会返回发送端的地址信息;
- send() 发送数据。接收参数为byte型,返回发送出去的字节长度。这个方法不需要指定接收端的地址信息,但存在sendto()方法,需要指定接收端的地址信息:
client.sendto(data, address)
; - close() 关闭套接字。
说明:accept()和recv()都会阻塞程序,通过socket.setblocking(False)
可以设置为非阻塞,但程序会抛BlockingIOError异常,可以用try-except忽略掉。
服务器与客户端
作为服务器所需步骤:
第一步绑定地址(bind),第二步设置监听(listen),第三步等待连接(accept),第四步循环接收数据与发送数据操作(send、recv)。
作为客户端所需步骤:
第一步连接服务器(connect),第二步循环接收数据与发送数据操作( send、recv)。
这里在网上找了一张流程图方便理解,侵删。
简易聊天系统
在这个聊天系统中,有以下几个要求:
- 服务器只能被一个客户端连接;
- 服务器与客户端可以任意时候发送数据和接收数据;
- 客户端通过输入q!或者Q!命令,实现退出聊天系统的操作;
- 客户端退出后,服务器进入等待连接状态,直到下一个客户端进入连接。
要求1很好实现,只需要listen(1)
即可。
对于要求2,不论是客户端还是服务器,因为需要“任意时候可以发送数据和接收数据”,涉及到的input
和recv
都会阻塞程序,所以需要抽象出两个方法send_data()和recv_data(),由两个线程执行,防止终端被霸占。
def send_data(sock):
while True:
words = input(">>")
...
sock.send(words.encode("utf-8"))
...
def recv_data(sock, addr):
while True:
...
data = sock.recv(1024)
...
# 格式化打印接收到的数据
dataUTF8 = data.decode("utf-8")
print("\r【时间:{time}】【来自:{ip}】\n【内容:{content}\n{separate}".format(
time=time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()),
ip="%s:%d" % addr,
content=dataUTF8,
separate="-"*50
))
print(">>", end="")
sys.stdout.flush() # 强刷管道,不然`>>`可能打印不出来
这里需要注意,多线程时print(">>", end="")
会打印不出来,但print(">>")
这种格式可以正常输出到屏幕。我知道是因为内容被缓存到了管道中,导致终端没有输出,然而根本原因不明确。不过sys.stdout.flush()
可以刷新管道,数据能被正常打印了。
要求3不难,只需要在send_data()中增加标准输入的检查,当输入的内容是q!或者Q!,退出循环,结束此线程。
# client.py
def send_data(sock):
while True:
words = input(">>")
if words in ("q!", "Q!"):
break
sock.send(words.encode("utf-8"))
执行send_data()的线程结束后,需要程序正常往下执行,所以主线程只等待send_data(),而不等待执行recv_data()函数的线程:
# client.py
sendthreading = threading.Thread(target=send_data, args=(client, ))
recvthreading = threading.Thread(target=recv_data, args=(client, ("127.0.0.1", 8080)))
sendthreading.start()
recvthreading.start()
sendthreading.join() # 只等待send_data()的线程结束
client.close()
win与linux的差别:
接下来需要区分win与linux中的区别。当send_data()结束,程序接下来会执行client.close()
,同时还有个子线程负责recv_data(),也就是说这个函数中还使用着套接字client。然而,套接字却被主线程关闭了——注意这个大前提。现在,在win中,当套接字被关闭,程序客户端(client.py)会抛异常ConnectionAbortedError,但linux中什么也不会发生;此时服务器与客户端之间的连接断开,win中,服务器端会在recv句中抛异常ConnectionResetError,而在linux中服务器的recv此时将一直接收空数据,导致程序服务器一直打印空数据。为了兼容两个系统,在server.py中需要判断接收到的数据是否为有效数据:
def recv_data(addr):
global clientsock
while True:
try:
data = clientsock.recv(1024)
# 兼容linux
if not data: # 如果不是有效数据,抛出异常
raise ConnectionResetError
except ConnectionResetError:
...
客户端里,当抛出ConnectionAbortedError时,跳出接收线程的循环:
# client.py
def recv_data(sock, addr):
while True:
try:
data = sock.recv(1024)
except ConnectionAbortedError:
break
...
当然还要考虑到服务器异常退出时,此时客户端——如果在linux系统,会一直接收到空数据,如果在win系统,会引发ConnectionResetError异常,所以继续完善上面代码:
# client.py
def recv_data(sock, addr):
while True:
try:
data = sock.recv(1024)
# 兼容linux
if not data:
raise ConnectionResetError("远程主机强迫关闭了一个现有的连接。")
except ConnectionAbortedError:
break
认为服务器退出之后,客户端应该抛错提示,所以不对ConnectionResetError做异常处理。
又因为在linux中客户端套接字被关掉,recv不会报错,它所在线程(负责recv_data()的线程)不会终止。正常情况下,主线程运行结束后等待子线程结束。但我们希望主线程结束后子线程也会跟着死亡,所以把该线程设置为守护。表示主线程结束,子线程跟着结束:
recvthreading.setDaemon(True) # 兼容linux
为满足要求4,在文件server.py中做以下准备:
# server.py
def recv_data():
global clientsock
while True:
try:
data = clientsock.recv(1024)
# 兼容linux
if not data:
raise ConnectionResetError
except ConnectionResetError:
# 用户退出后进入等待模式
print("\r【用户已退出】")
print("【等待用户连接】")
clientsock, addr = server.accept()
print("【接入用户】")
print(">>", end="")
continue
...
运行效果
此次完整代码已经上传Github,见简易聊天系统-多线程目录。
还不快抢沙发