Python使用Socket和多线程实现简单的TCP服务端和客户端通信

本文主要是使用Socket的方式进行Python的网络编程,结合多线程完成服务端同时连接多个客户端的程序,学习了解Socket的主要工作流程。

一、关于Socket

1.Socket简介

Socket是指套接字,是对网络中不同主机上的应用进程之间进行双向通信的端点的一种抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。

2.Socket的主要类型

Socket主要有三种类型:流套接字、数据报套接字、原始套接字。

  • 流套接字(SOCK_STREAM):采用了TCP协议,用于提供面向连接、可靠的数据传输服务。
  • 数据报套接字(SOCK_DGRAM):采用了UDP协议,提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
  • 原始套接字(SOCK_RAW):与上面两种套接字的区别在于原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。

本文中讲的是采用TCP协议,即流套接字。

3.Socket的工作流程

由于是双向通信,所以Socket的工作流程需要一对套接字连接进行使用,一个是作为服务端(Server),一个是作为客户端(Client)。Socket工作的基本流程和Python中主要的几个方法如下图所示。
在这里插入图片描述
服务端使用 socket()创建套接字之后,通过bind()方法绑定端口,然后使用listen()对端口进行阻塞式地监听,等待客户端发来建立连接的请求。当接收到建立连接的请求时,使用accept()方法接受客户端的连接请求,此后进入recv()send()不断进行接收数据和发送数据的操作。最后,使用close()关闭套接字终止程序,不过服务端程序一般不会主动进行关闭。
客户端相对来说比较简单,同样使用socket()close()来创建和关闭套接字。客户端使用connect()向目标的地址和端口发出建立连接的请求,建立连接成功之后就会进入recv()send()中不断进行接收数据和发送数据的操作。

二、Python的socket模块

Python中进行网络编程的主要是使用socket模块,当然还有高级一点的网络服务模块SocketServer等内容。本文中主要使用的是socket模块。
socket模块中首先需要使用socket()方法创建套接字对象,代码示例如下:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

其中,第一个参数是代表套接字家族,一般有socket.AF_UNIX、socket.AF_INET、socket.AF_INET6可以选择。AF_UNIX是本机的通信,AF_INET和AF_INET6分别是IPv4和IPv6。第二个参数是套接字类型,有socket.SOCK_STREAM、socket.SOCK_DGRAM、socket.SOCK_RAW,分别代表套接字的三种类型。
以下简要介绍socket对象中函数的使用描述。

1.服务端使用的函数

函数描述
s.bind()绑定地址(host,port)到套接字, 在 AF_INET下,以元组(host,port)的形式表示地址
s.listen()开始 TCP 监听。参数backlog是指操作系统可以挂起的最大连接数量。该值至少为 1,一般设置为5
s.accept()被动接受TCP客户端连接,阻塞式等待连接的到来

2.客户端使用的函数

函数描述
s.connect()TCP服务器连接,参数address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex()connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

3.服务端和客户端都可以使用的函数

函数描述
s.recv()接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量
s.send()发送 TCP 数据,将参数string 中的数据发送到连接的套接字
s.sendall()完整发送 TCP 数据。将参数 string 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回 None,失败则抛出异常。
s.close()关闭套接字
s.getpeername()返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)
s.getsockname()返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt()设置给定套接字选项的值
s.getsockopt()返回套接字选项的值
s.settimeout()设置套接字操作的超时期,参数timeout是一个浮点数,单位是秒。值为None表示没有超时期
s.gettimeout()返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None
s.setblocking()如果参数flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常

三、服务端程序

服务端的程序除了使用套接字的通信之外,还要使用多线程的方式来与多个客户端保持通信连接。因此,服务端的基本流程如下图。当每次建立起一个新的连接时,就使用threading模块创建一个新的线程,向新的线程中传入该客户端套接字的信息,并保持通信,同时该线程需要通过thd.setDaemon(True)设置为守护主线程。
在这里插入图片描述
在这里呢,为了体现简单地体现服务端对数据的处理和转发的功能,我假设了一个场景,是将客户端分为APP和设备两种,模拟一下设备通过服务端的转发向APP发送数据的情况。这里的设备可以理解为某种传感器或者某些被管理的计算机等。
在服务端程序中,创建了两个列表来储存当前在线的设备客户端和APP客户端,在连接创建和连接断开时更新列表的数据。为了识别客户端是APP还是设备,需要客户端在连接建立时发送一个字符串作为信号告诉服务端自己的身份,用dev代表设备,用app代表APP客户端。

# -*- coding:utf-8 -*-

import socket
import threading

tcp_port = 填写服务器端的端口号
client_num = 填写可以挂起的最大的客户端数量,一般为5
app_list = list()
dev_list = list()


def app_handle(app_client, data):
    print()


def dev_handle(dev_client, data):
    data = data.encode()
    for app in app_list:  # 转发设备的数据给APP
        app.send(data)


def message_handle(tcp_client, tcp_client_ip):
    client_type = ""  # 用于标记该客户端的身份
    while True:
        try:
            recv_data = tcp_client.recv(4096)
            if recv_data:
                recv_data = recv_data.decode()
                if recv_data == "dev":
                    dev_list.append(tcp_client)
                    client_type = "dev"
                    print(str(tcp_client_ip) + "客户端为设备")
                    print("设备:" + str(dev_list))
                elif recv_data == "app":
                    app_list.append(tcp_client)
                    client_type = "app"
                    print(str(tcp_client_ip) + "客户端为APP")
                    print("app:" + str(app_list))
                else:
                    if client_type == "app":
                        app_handle(tcp_client, recv_data)
                    elif client_type == "dev":
                        dev_handle(tcp_client, recv_data)
        except Exception as e:  # 连接出现异常的处理
            print(str(tcp_client_ip) + str(e))
            if client_type == "app":
                app_list.remove(tcp_client)
                print("app:" + str(app_list))
            elif client_type == "dev":
                dev_list.remove(tcp_client)
                print("设备:" + str(dev_list))
            break


if __name__ == '__main__':
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 使用TCP协议
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    server.bind(("", tcp_port))
    server.listen(client_num)

    while True:
        client, client_ip = server.accept()
        client.settimeout(120)  # 设置连接的超时期
        print(str(client_ip) + "接入")
        thd = threading.Thread(target=message_handle, args=(client, client_ip))
        thd.setDaemon(True)  # 设置守护主线程
        thd.start()

四、客户端程序

客户端程序相对比较简单,就是发送或者接收数据。
以下是APP的客户端的程序。

# -*- coding:utf-8 -*-
# APP端的程序
import socket

server_ip = 服务器的外网IP地址
server_port = 服务器端的端口

if __name__ == '__main__':
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.settimeout(120)
    client.connect((server_ip, server_port))
    client.send("app".encode())  # 告诉服务端自己的身份
    while True:
        print(client.recv(1024).decode())

以下是设备的客户端的程序。

# -*- coding:utf-8 -*-
# 设备端的程序
import socket

server_ip = 服务器的外网IP地址
server_port = 服务器端的端口

if __name__ == '__main__':
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.settimeout(120)
    client.connect((server_ip, server_port))
    client.send("dev".encode())  # 告诉服务端自己的身份
    client.send("testing".encode())


版权声明:本文为peanuo原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。