Socket编程

Python 小记 2019-02-10 5471 字 1380 浏览 点赞

前言

在网络编程里总会涉及到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)。

这里在网上找了一张流程图方便理解,侵删。

简易聊天系统

在这个聊天系统中,有以下几个要求:

  1. 服务器只能被一个客户端连接;
  2. 服务器与客户端可以任意时候发送数据和接收数据;
  3. 客户端通过输入q!或者Q!命令,实现退出聊天系统的操作;
  4. 客户端退出后,服务器进入等待连接状态,直到下一个客户端进入连接。

要求1很好实现,只需要listen(1)即可。

对于要求2,不论是客户端还是服务器,因为需要“任意时候可以发送数据和接收数据”,涉及到的inputrecv都会阻塞程序,所以需要抽象出两个方法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,见简易聊天系统-多线程目录。



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论