python socket

python socket

b站 奇乐编程学院 TCP/IP网络通信之Socket编程入门

单线程socket,socket入门代码

下面是一个socket例子,实现的是服务器接收到来自客户端的数据,又原封不动发送给客户端的功能。

服务器端:

服务器端绑定bind的ip地址可以是任意的,表示服务器端监听任意ip地址,也就是可以接收任意客户端的信息。

只有在客户端连接的端口号是bind里设置的端口号时,才会和服务器端连接上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 下面是一个socket例子,实现的是服务器接收到来自客户端的数据,又原封不动发送给客户端的功能。 

import socket
# AF_INET 表示我们使用的是IPv4的地址家族(address family)
# SOCK_STREAM 表示我们使用的是TCP协议(TCP是“流式”协议)

# 这里是服务器端的操作:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("0.0.0.0", 1234))
# bind()将我们创建的socket关联到我们主机的某一个网卡(也叫网络接口 network interface)和端口上。
# 网卡可以用ip地址指定
# 这里用 0.0.0.0 地址,表示主机上的任意网卡都可以使用这个socket进行通信。
# 表示服务器端监听任意ip地址,也就是任意客户端的信息
# 只有在客户端连接的端口号是1234时,才会和服务器端连接上。

s.listen() # 将socket置为监听状态,并等待客户端的连接

c, addr = s.accept() # 接受来自任意客户端的连接,并返回一个新的socket c,以及客户端的ip地址。
# 这个c是一个与之前s不同的socket
# socket s主要用于监听,server socket
# socket c用于与连接的客户端进行通信,client socket
with c:
print(addr, "connected.") # 打印客户端的IP地址
# 这里这个写法,暗示服务器端只能同时处理一个客户端的请求,先作为入门这样看,下面可以看到改进的并发版本。
while True:
data = c.recv(1024) # 一直调用recv()接收客户端来的信息
# 1024代表一次性接受数据的最大长度,1024个字节
if not data:
break
c.sendall(data) # 原封不动将数据传回给客户端

保存的该python文件,比如为server.py,然后在终端执行 python server.py,就启动了服务器端socket。

在新打开一个终端,输入 nc 127.0.0.1 1234 (127.0.0.1 是一个回送地址(lookback address),代表本地计算机,1234是端口号)。

按下回车,就可以看见服务器端收到了一个新连接,显示('127.0.0.1', 53046) connected ,表示接收到ip为127.0.0.1的客户端,客户端端口为53046.

然后在客户端窗口中随便输入字符串,服务器也会原封不动的发送回来。

image-20230201162825199

客户端:

1
2
3
4
5
6
7
import socket

with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.connect(("127.0.0.1",1234)) # 传入服务器端的ip和端口号
s.sendall(b"hello") # 发送一条信息给服务器端,这里参数是字节序列,要加b(不是字符串)
data = s.recv(1024) # 接收服务器返回的消息
print("received:", repr(data))

结果为:

image-20230201164000502

多线程的socket服务器

服务器并发的与多个客户端进行通信

1. 通过创建线程来响应不同客户端的请求

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 下面是一个多线程的socket例子,实现的是服务器接收到来自客户端的数据,又原封不动发送给客户端的功能。

import socket
import threading

def handle_client(c, addr):
print(addr, "connected.") # 打印客户端的IP地址

while True:
data = c.recv(1024) # 一直调用recv()接收客户端来的信息
# 1024代表一次性接受数据的最大长度,1024个字节
if not data:
break
c.sendall(data) # 原封不动将数据传回给客户端

# AF_INET 表示我们使用的是IPv4的地址家族(address family)
# SOCK_STREAM 表示我们使用的是TCP协议(TCP是“流式”协议)
# 这里是服务器端的操作:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("0.0.0.0", 1234))
# bind()将我们创建的socket关联到我们主机的某一个网卡(也叫网络接口 network interface)和端口上。
# 网卡可以用ip地址指定
# 这里用 0.0.0.0 地址,表示主机上的任意网卡都可以使用这个socket进行通信。

s.listen() # 将socket置为监听状态,并等待客户端的连接
while True:
c, addr = s.accept() # 在循环中不停调用accept接受来自客户端的连接
# 接受来自任意客户端的连接,并返回一个新的socket c,以及客户端的ip地址。

# 为了避免程序的阻塞(block),创建一个新线程
t = threading.Thread(target=handle_client, args=(c,addr)) # 新线程是 handle_client()
# 将客户端的socket c和地址传递给这个线程

t.start()

# 然后线程中的代码和单线程中的一样。

结果:

image-20230201172243140

多线程局限性:由于python的 GIL(https://python.land/python-concurrency/the-python-gil) 的存在,python中的线程其实做不到真正的并发,并且线程自身也会占用额外的系统资源(线程内存开销、线程切换开销)

除了线程之外,还可以使用基于事件驱动的selectors来实现多个连接的并发,或者通过更高层的asyncio来实现异步的socket代码。

简易HTTP服务器

HTTP是TCP协议的一个典型应用,也是浏览器与服务器交互的主要方式。通常服务器会监听80端口,然后等待客户端的连接(也就是浏览器比如搜索一个网址,是向服务器发起一次请求)。

客户端在连上服务器以后,首先需要指定要访问的资源,然后客户端会提供一系列额外的信息,这些信息每一条都是以冒号分隔的键/值对,信息是比如浏览器版本等,这些额外信息称作消息的头部(header)。随后是一个空行,再之后是消息的主体(body)(如果有的话)。

服务器在收到消息后,会以同样格式来响应客户端的请求,首先第一行是一个状态行(status line),里面包含一个状态码,比如200代表请求成功,404代表请求的资源不存在。接着同样是一系列键/值对,里面包含了请求资源的类型,服务器信息等。再后面是一个空行,再之后是消息的主体(body)(如果有的话)。

image-20230201175329315

http socket的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import socket
import threading

WEBROOT = "webroot" # 这里webroot指的是这个例子的python文件当前路径下里面的子文件夹,也可以是其它文件夹名称

def handle_client(c, addr):
print(addr, "connected.") # 打印客户端的IP地址

with c:
request = c.recv(1024) # 一直调用recv()接收客户端来的信息 # request是信息
# 1024代表一次性接受数据的最大长度,1024个字节

# Parse HTTP headers
headers = request.split(b"\r\n") # 将消息拆分成一行一行的字符串,存放在header这个列表中
# HTTP标准中定义的换行符是“回车+换行”
file = headers[0].split()[1].decode() # 提取出请求的文件名

# Load file content
# 如果用户请求的是根路径,直接返回index.html文件
if file == "/":
file = "/index.html"

# 读取文件内容,并返回一个状态号为200的消息
try:
with open(WEBROOT + file, "rb") as f:
content = f.read()
response = b"HTTP/1.0 200 OK\r\n\r\n" + content

# 如果请求的文件不存在
except FileNotFoundError:
response = b"HTTP/1.0 404 NOT FOUND\r\n\r\nFile not found"

# Send HTTP response
c.sendall(response)

# AF_INET 表示我们使用的是IPv4的地址家族(address family)
# SOCK_STREAM 表示我们使用的是TCP协议(TCP是“流式”协议)
# 这里是服务器端的操作:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("0.0.0.0", 80)) # 这里好像只能是80端口,表示浏览器监听的?
# bind()将我们创建的socket关联到我们主机的某一个网卡(也叫网络接口 network interface)和端口上。
# 网卡可以用ip地址指定
# 这里用 0.0.0.0 地址,表示主机上的任意网卡都可以使用这个socket进行通信。

s.listen() # 将socket置为监听状态,并等待客户端的连接
while True:
c, addr = s.accept() # 在循环中不停调用accept接受来自客户端的连接
# 接受来自任意客户端的连接,并返回一个新的socket c,以及客户端的ip地址。

# 为了避免程序的阻塞(block),创建一个新线程
t = threading.Thread(target=handle_client, args=(c,addr)) # 新线程是 handle_client()
# 将客户端的socket c和地址传递给这个线程

t.start()

# 然后线程中的代码和单线程中的一样。

在该python文件(比如socket_http.py)当前路径下,有一个子文件夹,取名叫webroot,里面有一些网页,代表客户端搜索不同的网址,会连接到服务器的哪些个网页里(这里服务器的路径指的就是该python文件)。

image-20230202101951030

然后启动 sudo python3 socket_http.py,在浏览器页面中输入 http://127.0.0.1/ 根路径,代码里要返回index.html这个文件,这个在网页中一般表示主页的意思,里面的内容也是自己定义的,这里”/“就是指的根目录。如果在浏览器中输入 http://127.0.0.1/123.HTML,服务器端会返回给浏览器(客户端)123.html这个文件。

在浏览器(客户端)和服务器建立请求时,不用特定指定端口了,80就是服务器默认端口了,浏览器要建立就直接是80端口了。(每次发送连接请求,客户端的端口各不相同)

index.html内容:

1
2
3
4
<html>
<meta charset="UTF-8">
<h1>这是一个主页</h1>
</html>

浏览器呈现:

image-20230202102752447

123.html内容:

1
2
3
4
<html>
<meta charset="UTF-8">
<h1>随便说点什么吧</h1>
</html>

浏览器呈现:

image-20230202102906931

如果输入一个没有的链接:

image-20230202103036882

这样就实现了一个浏览器http和服务器的socket通信。

python的标准库里已经实现了一个简易的HTTP服务器,它主要被用在开发和测试中,调用起来很方便,命令是:python -m http.server 8000