本篇文章是Network And Web Programing
-Socket Programing
分类中的第一篇文章,内容主要包含
一个socket确定了网络中两个应用的端口之间的唯一连接方式,一个socket包含三个部分:协议方式(TCP, UDP或IP)、IP地址和端口号PORT,端口号是一个整数代表着一个进程,为了唯一确定端口之间连接,还需要指定使用的协议类型(及其信息),这些唯一确定了两个结点之间的连接的信息就是socket,有时候socket和port会视作同义词来使用,但是需要注意两者是不同的
套接字编程是一种在网络中两个结点连接和交流的方式,其中一个socket(结点)绑定并监听一个特定IP的PORT的请求,另一个socket则向这个IP的PORT发送请求形成连接,监听的一方为服务端,主动发送连接请求的一段为客户端
连接百度服务端的代码示例
连接到一个服务端需要知道它的IP和开放连接的端口,连接时IP不能填域名,可以通过ping www.baidu.com
获取百度的IP,或者在代码中这样获取
import socket
ip = 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:
实现包含以下步骤
# server.py
import socket
def 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:
包含两个步骤
# client.py
import socket
def main():
ip = socket.gethostname()
port = 12345
ipaddress = (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'
server:
# -*- coding:utf-8 -*-
"""
一个简单的应答服务器
socketserver
TCPServer: 在socket之上封装的方便操作各种类型socket连接的类,默认是TCP连接
BaseRequestHandler: socket响应处理的基类,无实际实现功能
属性request: 属性是客户端socket,
属性client_address: 包含服务器绑的定IP和端口号
"""
import socket
import time
from socketserver import TCPServer, BaseRequestHandler
class EchoHandler(BaseRequestHandler):
def handle(self) -> None:
print(f"Got connection from address {self.client_address}")
self.request: socket.socket
while 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 time
def 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()
但是随着客户端的数量增加,非常有必要限制一下客户端连接的数量和缩短连接等待时间,在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 Thread
server = ThreadingTCPServer(("", 12345), EchoHandler, bind_and_activate=False)
# 预先分配好最大的工作线程池,每个服务处理一个连接并限制了客户端连接的最大数量
nworkers = 10
for i in range(nworkers):
t = Thread(target=server.serve_forever)
t.daemon = True
t.start()
server.serve_forever()
允许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 = True
server = ThreadingTCPServer(("", 12345), EchoHandler)
server.serve_forever()
ref: Understanding Socket Concept
ref: IP Family
ref: Socket types
ref: Socket Level options