本篇文章是Network And Web Programing
-Socket Programing
分类中的第一篇文章,内容主要包含
- Socket概念理解
- Socket programing介绍
- 一个简单的TCP协议的server-client程序
- 支持同时处理多个客户端简单server-client连接程序
- socket的常用选项使用
理解socket概念
一个socket确定了网络中两个应用的端口之间的唯一连接方式,一个socket包含三个部分:协议方式(TCP, UDP或IP)、IP地址和端口号PORT,端口号是一个整数代表着一个进程,为了唯一确定端口之间连接,还需要指定使用的协议类型(及其信息),这些唯一确定了两个结点之间的连接的信息就是socket,有时候socket和port会视作同义词来使用,但是需要注意两者是不同的
Socket编程
套接字编程是一种在网络中两个结点连接和交流的方式,其中一个socket(结点)绑定并监听一个特定IP的PORT的请求,另一个socket则向这个IP的PORT发送请求形成连接,监听的一方为服务端,主动发送连接请求的一段为客户端
连接百度服务端的代码示例
连接到一个服务端需要知道它的IP和开放连接的端口,连接时IP不能填域名,可以通过ping www.baidu.com
获取百度的IP,或者在代码中这样获取
import socketip = socket.gethostbyname("www.baidu.com")
print(ip) # 163.177.151.109
连接到服务端
ip = socket.gethostbyname("www.baidu.com")
port = 80 # 默认开放的端口
ipaddress = (ip, port)sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.connect(ipaddress)
print(f"Successfully connected to baidu server on port: {ipaddress}")
输出:
Successfully connected to baidu server on port: ('163.177.151.109', 80)
socket包含两个参数
第一个参数涉及到socket支持的IP地址族类,AF_INET表示ipv4类,对于socket只有下面三种族类可用
AF_UNIX
AF_INET
AF_INET6
AF_UNIX族类socket可以提供单个系统之间进程的交流,AF_UNIX族类支持数据流和数据报类型的socket(类型在第二个参数介绍)
AF_INET和AF_INET6族类socket可以提供不同系统之间进程的交流,也支持数据流和数据报类型的socket
第二个参数是socket的类型,SOCK_STREAM表示面向连接TCP的协议,socket的类型取决于两个结点之间传递的数据的性质(例如稳定性、顺序一致性和重复信息的处理方式等),以下是定义在unix系统sys/socket.h文件中标准的socket类型
/*Standard socket types */
#define SOCK_STREAM 1 /*virtual circuit*/
#define SOCK_DGRAM 2 /*datagram*/
#define SOCK_RAW 3 /*raw socket*/
#define SOCK_RDM 4 /*reliably-delivered message*/
#define SOCK_CONN_DGRAM 5 /*connection datagram*/
知道怎么建立一个socket连接之后,现在需要怎么使用socket连接发送数据,socket库支持socket使用sendall方法发送数据,客户端可以发送数据,服务端也可以用这个方法发送数据
一个简单的server-client程序
server:
实现包含以下步骤
- 服务端需要使用bind()方法绑定到一个特定的IP和端口号
- 使用listen()方法监听这个端口
- 当接收到连接请求时使用accept()方法初始化一个socket的连接
- 然后对这个连接使用recv方法接收客户端发送的数据并交给程序处理
- 最后使用close()方法关闭socket连接
# server.py
import socketdef mian():ip = socket.gethostname()ipaddress = (ip, 12345)# ipaddress = ("", 12345) # socket bind的ip传入空字符串时让服务器可以监听网络中其他电脑的请求sk = socket.socket()sk.bind(ipaddress)sk.listen(5) # 指定系统允许的最大连接数,超过时会拒绝新的连接while True:conn, addr = sk.accept()conn.send(b"Got a connection from server")data = conn.recv(1024)print(f"received data from client: {data}")conn.close()if __name__ == "__main__":mian()
在命令行运行server.py后开启服务,等待客户端的连接请求
client:
包含两个步骤
- 创建socket对象
- 连接到指定的ip地址
# client.py
import socketdef main():ip = socket.gethostname()port = 12345ipaddress = (ip, port)sk = socket.socket()sk.connect(ipaddress)sk.send(b"Hello")data = sk.recv(1024)print(f"receive data from server: {data}")sk.close()if __name__ == "__main__":main()
在另外一个命令行运行client.py,就会向server发送请求
# server output:
received data from client: b'Hello'
received data from client: b'Hello'
...# client output:
receive data from server: b'Got a connection from server'
使用在socket之上封装的socketserver编写TCP连接以及多线程支持多个客户端连接
server:
# -*- coding:utf-8 -*-
"""
一个简单的应答服务器
socketserverTCPServer: 在socket之上封装的方便操作各种类型socket连接的类,默认是TCP连接
BaseRequestHandler: socket响应处理的基类,无实际实现功能属性request: 属性是客户端socket,属性client_address: 包含服务器绑的定IP和端口号
"""
import socket
import time
from socketserver import TCPServer, BaseRequestHandlerclass EchoHandler(BaseRequestHandler):def handle(self) -> None: print(f"Got connection from address {self.client_address}")self.request: socket.socketwhile True:msg = self.request.recv(2048)print(f"Message from client: \n{msg}")self.request.send(msg)if __name__ == '__main__':server = TCPServer(("", 12345), EchoHandler) # TCPServer中实现了TCP服务器中的bind、listen和close等基本初始化操作server.serve_forever()
client:
# -*- coding:utf-8 -*-
import socket
import timedef connection():s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.connect(("localhost", 12345))while True:msg = input("Input msg and send:\n")s.send(msg.encode())print(f"Msg sent")time.sleep(1)if __name__ == '__main__':connection()
开启server服务之后,在client发送信息
# client
Input msg and send:
a
Msg sent
b
Msg sent
Input msg and send:
# server
Got connection from address ('127.0.0.1', 36087)
Message from client:
b'a'
Message from client:
b'b'
这个服务默认一次只能服务一个客户端,如果你尝试再打开一个client进程连接server时,发送消息后server端并不会立马收到消息,而是要等到第一个连接的client断开之后才会收到第二个client发送的消息,而且发送的多条消息会聚集到一条一种
让服务器支持同时处理多个客户端
如果想要一个服务器能同时处理和服务多个客户端,可以初始化一个ForkingTCPServer
或ThreadingTCPServer
if __name__ == '__main__':# server = TCPServer(("", 12345), EchoHandler) # TCPServer中实现了TCP服务器中的bind、listen和close等基本初始化操作server = ThreadingTCPServer(("", 12345), EchoHandler)server.serve_forever()
然后client开启两个进程连接server并发送消息:
限制客户端连接数
但是随着客户端的数量增加,非常有必要限制一下客户端连接的数量和缩短连接等待时间,在linux上可以使用iptable限制连接数和sysctl限制TIME_WAIT时间
限制一个IP最多15个连接:
-A INPUT -p tcp -m tcp --dport 12345 --tcp-flags FIN,SYN,RST,ACK SYN -m connlimit --connlimit-above 15 --connlimit-mask 32 --connlimit-saddr -j REJECT --reject-with tcp-reset
TCP连接超时时间设置为15s:
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
也可以使用多线程开启多个TCPServer
的方式来实现支持处理多个客户端的情况:
if __name__ == '__main__':from threading import Threadserver = ThreadingTCPServer(("", 12345), EchoHandler, bind_and_activate=False)# 预先分配好最大的工作线程池,每个服务处理一个连接并限制了客户端连接的最大数量nworkers = 10for i in range(nworkers):t = Thread(target=server.serve_forever)t.daemon = Truet.start()server.serve_forever()
socket的一些常用选项
允许socket的bind方法重复使用local address
if __name__ == '__main__':server = TCPServer(("", 12345), EchoHandler, bind_and_activate=False)# SOL_SOCKET指定选项类型/等级并设定SO_REUSEADDR的值为真指定该socket支持重复使用地址server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)server.server_bind()server.server_activate()server.serve_forever()
上面这个选项由于经常被使用到,它被放到TCPServer
的allow_reuse_address
属性,因此使用TCPServer
时可以直接修改socket重复使用local address的选项:
if __name__ == '__main__':ThreadingTCPServer.allow_reuse_address = Trueserver = ThreadingTCPServer(("", 12345), EchoHandler)server.serve_forever()
ref: Understanding Socket Concept
ref: IP Family
ref: Socket types
ref: Socket Level options