网络编程(二):服务端获取连接请求

在上一篇中,我们学习了如何在server上创建一个TCP的套接字,这里来看一看对于服务端,server是怎么获取客户端的连接请求的。

获取连接请求

在开启监听(listen)后,如果有客服端尝试连接服务器,那么内核将于客户端进行连接,因为请求连接可能会有多个,所以内核会维护一个队列来存放这些请求。当客户端连接到内核之后,那么内核可以使用accept函数来返回并接受来自这个连接。

下面来用代码演示一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 把上次的代码复制一下
from socket import *
# 创建一个套接字
# 使用socket进行初始化
serverSocket = socket(AF_INET, SOCK_STREAM) # 使用IPV4地址簇,使用的是流式socket

# 接下来开始进行绑定
serverSocket.bind(("127.0.0.1", 8080)) # bind 需要的是一个tuple类型

# 绑定后可以开始listen, 即查看是否有客户端连接到服务器
serverSocket.listen(1) # 最多监听一个

在创建好server socket之后,我们就可以使用accept函数来进行连接。这里先不考虑TCP协议在连接时的一些细节

1
2
3
4
5
6
'''
返回值: 
connectionSocket 客户端连接套接字
addr 连接的客户端地址
'''
connectionSocketaddr = serverSocket.accept()

也就是说,这个操作会返回一个新的socket,不同的是,通过这个socket就可以实现serverclient之间的通讯。

可以接受或者发送数据,下面是一些API

1
2
3
recv()/send()
recvmsg()/sendmsg()
recvfrom()/sendto()

需要注意的是,传递的这些字符全部都是流式数据,与原始字符串不同

简单的demo

在这里,创建一个简单的小demo,我们可以创建一个简单的服务程序,这个server什么也不做,只是简单的把接受到的数据原封不动的发送给client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from socket import *
# 初始化好一个socket
with socket(AF_INET, SOCK_STREAM) as serverScoket:
    # 绑定IP和port
    serverScoket.bind(("127.0.0.1", 8080))
    # 开启监听listen
    serverScoket.listen(1) # 这是一个监听队列,当处理多个请求的时候,会把未来得及处理的放入队列里面,其中的参数表示队列的大小
    # 开启socket的accept,从而处理来自server的连接
    connectionSocket, _ = serverScoket.accept() # 先忽略第二个返回值
    print('connect!')
    with connectionSocket as c:
        while True:    
            data = c.recv(1024) # 每次都尝试获得来自客户端的数据, 注意这是个字节流数据
            if not data:
                break
            c.sendall(data) # 把接受到的数据返回
            

我们可以使用netcat这个工具来进行测试

1
nc 1270.0.0.1 8080

可以看到,服务器端口已经连接上了

1
connect!

通过这个有趣的连接,我们还可以使用eval函数来实现计算式求值

eval函数是危险的!这里只是做演示

eval() 可以执行任意的 Python 代码。如果传入的字符串包含恶意代码,这可能导致严重的安全漏洞。例如,攻击者可以通过构造恶意表达式来执行系统命令、访问敏感数据、修改文件等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from socket import *
# 初始化好一个socket
with socket(AF_INET, SOCK_STREAM) as serverScoket:
    # 绑定IP和port
    serverScoket.bind(("127.0.0.1", 8080))
    # 开启监听listen
    serverScoket.listen(1) # 这是一个监听队列,当处理多个请求的时候,会把未来得及处理的放入队列里面,其中的参数表示队列的大小
    # 开启socket的accept,从而处理来自server的连接
    connectionSocket, _ = serverScoket.accept() # 先忽略第二个返回值
    print('connect!')
    with connectionSocket as c:
        while True:    
            data = c.recv(1024) # 每次都尝试获得来自客户端的数据, 注意这是个字节流数据
            if not data:
                break
             # 解码接收到的数据
            expression = data.decode()
            
            # 计算表达式的结果
            result = str(eval(expression))+'\n'
            
            # 发送结果给客户端
            c.sendall(result.encode())

一些缺点

浪费性能

对于真实世界来说,这里的服务器实在是太弱了

1
2
3
4
5
while True:    
        data = c.recv(1024)
        if not data:
            break
        c.sendall(data) # 把接受到的数据返回

server每次只能处理一个请求,当client没有发送数据的时候,c.recv(1024)这行代码也就会永远阻塞在这里,很浪费性能。

此时,很自然的想到以并发的方式去处理频繁的连接

应对策略

多进程

可以使用fork来创建多个进程,其中,fork函数在子进程里面的返回值是0,所以,可以设想一下,当有服务来临时,父进程只去监听(accept)是否有连接,同时可以把读写操作放到子进程里面去运行,这样通过进程调度策略,就可以实现并发

代码看起来是这样的:

1
2
3
4
5
6
7
8
while (true) {
    pid_t pid;
    if ((pid = fork()) == 0) { // 子进程
        // do read() or do write()
    } else {
        // accept 
    }
}

每当一个连接到来时,程序就会创建一个子进程来处理,可惜进程实在是不够“轻量”,而且进程调度器来进行调度的时候也需要有着内核态到用户态的转换、进程的变量读写保存,因此可以考虑使用多线程来进行尝试

多线程

什么是线程?

In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system.[1] In many cases, a thread is a component of a process.

The multiple threads of a given process may be executed concurrently (via multithreading capabilities), sharing resources such as memory, while different processes do not share these resources. In particular, the threads of a process share its executable code and the values of its dynamically allocated variables and non-thread-local global variables at any given time.

The implementation of threads and processes differs between operating systems.

线程是进程的一个子集(你可以这么认为),一个进程里面会有多个线程,这些线程共享这个进程所有的资源(因为它们有着一样的页表),所以在线程切换的时候,不需要有很大的开销,只需要维护每个线程内部不共享或者私有的数据即可。

这样就可以使用多线程来处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import threading
from socket import *

# 把发送请求发送这里
def handle_client(c, addr):
    print(addr, 'connect')
    while True:
        data = c.recv(1024)
        if not data:
            break
        c.sendall(data)

with socket(AF_INET, SOCK_STREAM) as serverSocket:
    serverSocket.bind(("127.0.0.1", 8080))
    serverSocket.listen()
    while True:
        connectionSocket, addr = serverSocket.accept() # 接受多个不同的请求
        t = threading.Thread(target=handle_client, args=(connectionSocket, addr))
        t.start()

多线程的方法可以使用线程池的方法去调度,不过线程也会占用系统的资源。

结尾

之后记录一下select、poll、epoll这些方法

Licensed under CC BY-NC-SA 4.0
花有重开日,人无再少年
使用 Hugo 构建
主题 StackJimmy 设计