小白服务器开发-socket网络编程

1. socket产生背景

在计算机网络发展过程中,随着网络规模的扩大和应用需求的增加,出现了不同主机上的进程需要相互通信以实现数据交换、资源共享、协同工作等场景。例如,在早期的网络环境中,如果一台计算机上的一个程序想要获取另一台计算机上某个程序的数据或者需要远程控制另一台计算机上的资源等,这就需要一种能够实现跨主机进程间通信的机制,于是 Socket 就应运而生了。
Socket 为不同主机之间的进程提供了一种统一的接口,使得进程可以通过网络进行数据的发送和接收。它就像是一个两端分别连接着两个不同主机上进程的通信管道,屏蔽了网络通信底层的复杂细节,让程序员可以通过简单的 API(应用程序编程接口)来实现跨主机的进程间的数据传输,从而有效地解决了跨主机进程间通信的问题,满足了日益增长的网络应用需求,广泛应用于各种网络应用领域,如 web 服务器与浏览器之间的通信、即时通讯软件中的消息传递等场景。

2. scoket使用流程

  1. 创建Socket:在客户端和服务器端,首先需要创建一个Socket对象。这可以通过调用socket库的相关函数完成。
  2. 绑定与监听(服务器端):服务器端的Socket需要绑定到一个特定的端口和地址上,并设置为监听模式,以等待客户端的连接请求。
  3. 发起连接(客户端):客户端的Socket需要指定服务器的地址和端口,并发起连接请求。
  4. 接受连接(服务器端):服务器接受客户端的连接请求,并创建一个新的Socket用于与该客户端的通信。
  5. 数据传输:一旦建立了连接,双方就可以通过各自的Socket发送和接收数据。

3. socket编程

3.1 客户端编程

首先初始化一个socket对象,通过socket对象我们可以与其他主机的服务进行通信了,需要设置一下通信协议。我们知道网络之间传输的是二进制,我们发了一个消息给服务器,服务要怎么把这个二进制还原成数据呢?就是通过约定好的协议,这里设置的是TCP协议。

import socket
client_socket = socket.socket(
    # 表示使用 IPv4,如果是 socket.AF_INET6
    # 则表示使用 IPv6
    socket.AF_INET,
    # 表示建立 TCP 连接,如果是 socket.SOCK_DGRAM
    # 则表示建立 UDP 连接
    socket.SOCK_STREAM
)

然后我们传入想要访问的服务端ip和端口,并开始连接

host = socket.gethostname()
port = 12345
client_socket.connect((host, port))

连接成功后我们就可以给服务器发消息了

while True:
    message = input("Enter message (or 'exit' to quit): ")
    if message.lower() == 'exit':
        break
    client_socket.send(message.encode())
    response = client_socket.recv(1024)
    print(f"Server response: {response.decode()}")

正常情况下,当输入exit就会退出while循环,也就不再与客户端通信了,这个时候需要关闭socket的连接

client_socket.close()

但是如果服务器挂了,也会直接断开客户端的连接,send和recv就会抛出异常,这个时候就会抛出异常,导致socket无法正常关闭,我们可以使用try except来解决,全部代码如下所示

import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = socket.gethostname()
port = 12345
client_socket.connect((host, port))

try:
    while True:
        message = input("Enter message (or 'exit' to quit): ")
        if message.lower() == 'exit':
            break
        client_socket.send(message.encode())
        response = client_socket.recv(1024)
        print(f"Server response: {response.decode()}")
except Exception as e:
    print(f"Error: {e}")
finally:
    client_socket.close()
    print("Connection closed")

客户端的逻辑就开发完成了,但是这个时候还不行,因为要等服务器启动之后才可以连接通信,直接执行就会报连接失败的错误。

ConnectionRefusedError: [Errno 61] Connection refused

3.2 服务端编程

服务端编程也很简单,同样是定义一个socket对象,通信协议要与服务端保持一致,就好比客户端说的是中文,服务端也得听懂中文才行,大家协议一定要对齐。

import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

然后就需要绑定一个端口,后续的数据都从这个端口进出,相当于占了一个码头,通过listen函数将自己设置为监听模式,也就是告诉操作系统其他客户端可以通过这个端口给socket传递数据,这里设置最多连接5个,再多就要排队了。

host = socket.gethostname()
port = 12345
server_socket.bind((host, port))
server_socket.listen(5)
print("Server is listening...")

一切设置好之后,服务器就可以接客了,通过accept函数等待客户端的连接,如果有连接就返回客户端的地址,同时返回一个socket对象用来跟客户端交互。感觉server_socket就像是大堂经理,等待客户的到来,来一个客户后,就会指派派一个业务员与客户进行对接,然后大堂经理再等待下一个客户到来

while True:
    conn, addr = server_socket.accept()
    print(f"Connection from {addr}")
    while True:
        data = conn.recv(1024)
        if not data: break
        print(f"Received message: {data.decode()}")
        conn.send(data)  # Echo back the received message
    conn.close()

启动服务端后就可以与客户端愉快的通信了。

4 并发连接访问

我们启动两个客户端与服务相连,你会发现只有一个客户端可以与服务器连接通信,另一个虽然不会报错,但是发现服务器压根就不搭理,简单分析一下服务端代码就知道,当accept收到某个用户的连接时候,后续操作直接进入了while True的死循环。压根就走不到第二次的accept。一拍脑子改成了这样,把while True给去掉了

while True:
    conn, addr = server_socket.accept()
    print(f"Connection from {addr}")
    data = conn.recv(1024)
    if not data: break
    print(f"Received message: {data.decode()}")
    conn.send(data)  # Echo back the received message
    # conn.close()

收到第一个客户端的连接,然后recv数据,并且发送响应。然后就到了下一个循环等待另一个客户端的连接,recv第二个客户端的数据并发送响应。然后就再等下一个客户端连接。这么一改就会发现,第一个客户端发送的数据再也收不到了,因为conn是最新连接的客户端。

4.1 多线程并发处理

以上的改动相当于只有一个业务员,这个业务员服务完你后,就去服务别人了,虽然你手上还拿着凭证,但是服务员移情别恋了。每来一个客户端连接我们就安排一个业务员对接,业务员就叫做thread吧,再拍一下脑袋

import threading
import socket

def handle_client(sock, addr):
    try:
        print(f'New connection from {addr}')
        while True:
            data = sock.recv(1024)
            if not data or data.decode() == 'exit':
                print(f"No data received from {addr}, closing connection")
                break
            print(f"Received from {addr}: {data.decode()}")
            sock.send(data)  # Echo back the received message
    except Exception as e:
        print(f"Error handling client {addr}: {e}")
    finally:
        sock.close()
        print(f'Connection from {addr} closed.')

def server():
    host = socket.gethostname()
    port = 12345

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind((host, port))
    server_socket.listen(5)
    print("Server is listening...")
    try:
        while True:
            sock, addr = server_socket.accept()
            client_thread = threading.Thread(target=handle_client, args=(sock, addr))
            client_thread.start()
    except KeyboardInterrupt:
        print("Server is shutting down.")
    finally:
        server_socket.close()

if __name__ == "__main__":
    server()

4.2 阻塞与非阻塞

分析上面代码,主要的原因就是因为accept和recv会阻塞主现场,当服务端执行到accept到时候,如果没有客户端连接,那么就一直阻塞在accept,没有办法走到recv来接收数据,socket提供了非阻塞的方式。首先就是创建非阻塞的socket接口

import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setblocking(False)
host = socket.gethostname()
port = 12345
server_socket.bind((host, port))
server_socket.listen(5)

setblocking(False) 将套接字设置为非阻塞模式,这样 accept() 和 recv() 方法不会阻塞,而是立即返回,如果没有数据或连接请求,会抛出 BlockingIOError 异常。然后就可以等待其他客户端的连接

connections = {}
while True:
    try:
        client_socket, client_address = server_socket.accept()
        print(f"New connection from {client_address}")
        connections[client_socket] = client_address
        client_socket.setblocking(False)
    except BlockingIOError as e:
        pass

如果有客户端连接,accept就会返回socket和地址,我们将这个client_socket存储起来后面使用。如果没有连接那么就抛出异常,我们捕获到这个异常之后直接pass就行。之后遍历所有的socket看看有没有数据到来

    connections_list = list(connections.items())
    for client_socket, client_address in connections_list:
        try:
            data = client_socket.recv(1024)
            if not data:
                print(f"Connection from {client_address} closed")
                client_socket.close()
            print(f"Received from {client_address}: {data.decode()}")
            client_socket.send("please jixu".encode())
        except BlockingIOError as e:
            pass
        except Exception as e:
            print(f"Connection from {client_address} Exception closed")
            client_socket.close()
            connections.pop(client_socket)

通过client_socket.setblocking(False)我们将client_socket也设置成非阻塞的了,如果调用recv没有接收到数据直接抛出BlockingIOError异常,不会卡在recv那里,捕获异常后代码可以继续执行。当然,如果抛出了其他异常说明连接出现了问题,我们直接将client_socket关闭,并从连接池中弹出。

使用 client_socket.setblocking(False) 将套接字设置为非阻塞模式。
在非阻塞模式下,recv 会立即返回,如果没有数据可接收,会抛出 BlockingIOError
如果 recv 返回空字节串 (b’'),表示对端关闭了连接,服务器需要关闭套接字并从 connections 中移除。

4.3 多路复用

假如我们买了冰箱,洗衣机,烘干机这三个商品,想要看看什么时候到快递站,一种最简单的方式就是给快递站打电话,傻乎乎的一直询问

while True:
	for 快递 in [冰箱,洗衣机,烘干机]:
		拨通电话(陷入内核态)
		询问快递是否到了(内核查看该socket的状态和数据)
		挂断电话(返回用户态)

应该没有正常人会这么干吧,正常人应该是这么干的

while True:
	告诉快递站要查询的快递[冰箱,洗衣机,烘干机](陷入内核态)
	快递站点帮忙查询所有的快递(内核轮询查看所有socket的状态和数据)
	告诉你哪些快递到了(返回用户态)

每次只需要打一个电话就行,快递小哥一次就给你查完了,有快递就通知你去取就行

直接使用非阻塞的套接字会有严重性能问题,我们手动维护了一个连接链表,然后不断地轮询每个套接字以检查是否可以进行读写操作。这种轮询方式会大量占用 CPU 时间,即使没有数据可读或可写,CPU 也会不断地执行轮询逻辑,而且每次查询套接字状态的时候都会从用户态切换到内核态,导致 CPU 资源浪费。
多路复用允许服务器在一个线程或进程中同时监控多个套接字的事件。服务器可以在一个循环中同时等待多个套接字变为可读或可写状态。一旦有套接字准备好,就可以立即进行读写操作,无需为每个连接单独轮询或阻塞等待。

首先还是定义服务端套接字

import socket
import select

# 创建服务器Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置套接字属性,这里让端口释放后立刻就能再次使用
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 设置为非阻塞模式
server_socket.setblocking(False)

# 绑定到本地地址和端口
host = socket.gethostname()
port = 12345
server_socket.bind((host, port))

# 开始监听
server_socket.listen(5)

print(f"Server listening on {host}:{port}")

然后就是创建待读写的socket套接字list,让内核帮我们统一查询

# 初始化文件描述符集合
read_fds = [server_socket]  # 需要监控读事件的套接字列表
write_fds = []  # 需要监控写事件的套接字列表(可选)
except_fds = []  # 需要监控异常事件的套接字列表(可选)
timeout = None  # select 的超时时间,单位为秒

# 用于存储已连接的客户端套接字和地址的映射
connections = {}

使用 select 监控事件:

  • read_fds:监控所有需要读操作的套接字,包括服务器 Socket(用于接收新连接)和客户端 Socket(用于接收数据)。
    select.select() 函数会阻塞直到有事件发生或超时。
try:
    while True:
        # 使用select监控可读、可写和异常事件
        readable, writable, exceptional = select.select(read_fds, write_fds, read_fds, timeout)
        # 处理可读事件
        for sock in readable:
            if sock == server_socket:
                # 处理新连接
                client_socket, client_address = sock.accept()
                client_socket.setblocking(False)
                print(f"New connection from {client_address}")
                connections[client_socket] = client_address
                read_fds.append(client_socket)
            else:
                # 处理客户端数据
                data = sock.recv(1024)
                if not data:
                    # 客户端关闭连接
                    print(f"Connection closed by {connections[sock]}")
                    sock.close()
                    read_fds.remove(sock)
                    connections.pop(sock)
                else:
                    print(f"Received from {connections[sock]}: {data.decode()}")

        # 处理异常事件
        for sock in exceptional:
            print(f"Exceptional condition on {connections.get(sock, sock)}")
            sock.close()
            if sock in read_fds:
                read_fds.remove(sock)
            if sock in connections:
                connections.pop(sock)

except KeyboardInterrupt:
    print("\nServer shutting down.")

finally:
    # 关闭所有连接和服务器Socket
    for sock in connections:
        sock.close()
    server_socket.close()
    print("All connections and server socket closed.")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值