代码改变世界

python 服务器和客户端通信

2022-04-05 16:51  jym蒟蒻  阅读(1518)  评论(1编辑  收藏  举报

python 实现TCP socket通信和 HTTP服务器、服务器和客户端通信实例

    • socket是什么?
    • 服务器和客户端通信的流程
    • python 实现TCP socket通信例子
    • 关于Host和PORT的设置
    • socket函数
    • socket编程思路
    • 基于TCP socket的HTTP服务器
    • 分析HTTP服务器代码
      • 服务器的response文本
      • 客户端的request文本
    • 分析运行结果

 

socket是什么?

由下图可理解:Socket是应用层与TCP/IP协议族通信的中间软件抽象层。

复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

在这里插入图片描述

服务器和客户端通信的流程

由下图可理解服务器和客户端通信的流程:

服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。

在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。

客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

在这里插入图片描述

网络中进程之间通信:

首要解决的问题是如何唯一标识一个进程。

TCP/IP协议族已经帮我们解决了这个问题,

网络层的“ip地址”可以唯一标识网络中的主机,

而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。

这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

一个socket包含四个地址信息: 两台计算机的IP地址和两个进程所使用的端口(port)。IP地址用于定位计算机,而port用于定位进程。

python 实现TCP socket通信例子

在互联网上,我们可以让某台计算机作为服务器。

服务器开放自己的端口,被动等待其他计算机连接。

当其他计算机作为客户,主动使用socket连接到服务器的时候,服务器就开始为客户提供服务。

在Python中,我们使用标准库中的socket包来进行底层的socket编程。

服务器端:使用bind()方法来赋予socket以固定的地址和端口,并使用listen()方法来被动的监听该端口。当有客户尝试用connect()方法连接的时候,服务器使用accept()接受连接,从而建立一个连接的socket:

import socket

HOST = ''
PORT = 8000

reply = 'Yes'

'''
socket.socket()创建一个socket对象,
并说明socket使用的是IPv4(AF_INET,IP version 4)
和TCP协议(SOCK_STREAM)。
'''
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))

s.listen(3)

conn, addr = s.accept()

request = conn.recv(1024)

print('request is:', request.decode())
print('Connected by:', addr)

conn.sendall(reply.encode())

conn.close()


客户端:主动使用connect()方法来搜索服务器端的IP地址和端口,以便客户可以找到服务器,并建立连接:

import socket

HOST = '127.0.0.1'
PORT = 8000

request = 'can you hear me?'

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

'''
TypeError: a bytes-like object is required, not 'str'
解决办法:
str→bytes:encode()方法。str通过encode()方法可以转换为bytes。
bytes→str: decode()方法。bytes通过decode()方法可以转换为str。
'''
s.sendall(request.encode())

reply = s.recv(1024)

# send 函数的参数和 recv 函数的返回值都是 bytes 类型

print('reply is:', reply.decode())
s.close()

服务器端运行结果:

request is: can you hear me?
Connected by: ('127.0.0.1', 1489)

客户端运行结果:

reply is: 'Yes'

关于Host和PORT的设置

我本机的ipv4地址是10.1.173.8。我把服务器和客户端都放到一个电脑上。

我测试了一下,在客户端代码中,我设置HOST = ‘10.1.173.8’。

然后,运行客户端和服务器代码,产生和HOST = '127.0.0.1’同样结果。

也就是说,客户端的HOST,为客户端IP想要connect的IP。

为了证明这一想法,我把客户端设置成HOST = ‘10.1.172.1’

再次运行,出现报错:

s.connect((HOST, PORT))
TimeoutError: [WinError 10060] 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。

可见,如果没有两台计算机做实验,可以将客户端IP想要connect的IP改为"127.0.0.1",这是个特殊的IP地址,用来连接当地主机。

而且,如果知道服务器的IP,那么也就可以在客户端直接设置HOST = 服务器的ip

下面我测试一下端口号PORT:

服务器端和客户端 端口号都相同时候,可以建立连接。而且端口的数不会影响连接,都是8000和都是8080,都能连上。

端口号不同就连接不上。

socket函数

服务端socket函数描述
s.bind(address) 将套接字绑定到地址, 在AF_INET下,以元组(host,port)的形式表示地址.
s.listen(backlog) 开始监听TCP传入连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept() 接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
客户端socket函数描述
s.connect() 主动初始化TCP服务器连接。一般address的格式为元组(主机/ip,port),如果连接出错,返回socket.error错误。
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共socket函数描述
s.recv() socket.recv(bufsize[, flags]),从套接字接收数据。返回值是表示接收到的数据的bytes对象。bufsize指定一次接收的最大数据量。一般为1024开始
s.send() 发送数据,将数据发送到socket套接字。(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.close() 关闭socket 套接字

socket编程思路

TCP服务端:

1 创建套接字,绑定套接字到本地IP与端口

# socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.bind()

2 开始监听连接 #s.listen()

3 进入循环,不断接受客户端的连接请求 #s.accept()

4 然后接收传来的数据,并发送给对方数据 #s.recv() , s.sendall()

5 传输完毕后,关闭套接字 #s.close()

TCP客户端:

1 创建套接字,连接远端地址

​ # socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.connect()

2 连接后发送数据和接收数据 # s.sendall(), s.recv()

3 传输完毕后,关闭套接字 #s.close()

基于TCP socket的HTTP服务器

上面的例子中,使用TCP socket来为两台远程计算机建立连接。

然而,socket传输自由度太高,从而带来很多安全和兼容的问题。

往往利用一些应用层的协议(比如HTTP协议)来规定socket使用规则,以及所传输信息的格式。

HTTP协议利用请求-回应(request-response)的方式来使用TCP socket。

客户端向服务器发一段文本作为request,服务器端在接收到request之后,向客户端发送一段文本作为response。

在完成了这样一次request-response交易之后,TCP socket被废弃。

下次的request将建立新的socket。

request和response本质上说是两个文本,只是HTTP协议对这两个文本都有一定的格式要求。

import socket

HOST = ''
PORT = 8000

text_content = b'''HTTP/1.x 200 OK  
Content-Type: text/html

<head>
<title>WOW</title>
</head>
<html>
<p>Wow, Python Server</p>
<IMG src="test.jpg"/>
</html>
'''
f = open('test.jpg', 'rb')
pic_content = b'''
HTTP/1.x 200 OK  
Content-Type: image/jpg

'''
pic_content = pic_content+f.read()
f.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))

while True:
    s.listen(3)
    conn, addr = s.accept()
    request = conn.recv(1024)
    method = request.decode().split(' ')[0]
    src = request.decode().split(' ')[1]

    if method == 'GET':
        if src == '/test.jpg':
            content = pic_content
        else:
            content = text_content
        print('Connected by', addr)
        print('Request is:', request)
        conn.sendall(content)

    conn.close()

为了配合上面的服务器程序,在放置Python程序的文件夹里,保存了一个test.jpg图片文件。

在终端运行上面的Python程序,作为服务器端。

再打开一个浏览器作为客户端。

在浏览器的地址栏输入:127.0.0.1:8000,也可以用另一台电脑,并输入服务器的IP地址。

可以看到:

在这里插入图片描述

分析HTTP服务器代码

服务器的response文本

如我们上面所看到的,服务器会根据request向客户传输两条信息text_content和pic_content中的一条,作为response文本。

整个response分为**起始行(start line), 头信息(head)和主体(body)**三部分。

起始行就是第一行:

HTTP/1.x 200 OK

它实际上又由空格分为三个片段,HTTP/1.x表示所使用的HTTP版本,200表示状态(status code),200是HTTP协议规定的,表示服务器正常接收并处理请求,OK是供人来阅读的status code。

头信息跟随起始行,它和主体之间有一个空行。这里的text_content或者pic_content都只有一行的头信息,text_content的头信息

Content-Type: text/html

表示主体信息的类型为html文本

而pic_content的头信息

Content-Type: image/jpg

说明主体的类型为jpg图片(image/jpg)。

主体信息为html或者jpg文件的内容。

(注意,对于jpg文件,我们使用’rb’模式打开,是为了与windows兼容。因为在windows下,jpg被认为是二进制(binary)文件,在UNIX系统下,则不需要区分文本文件和二进制文件。)

客户端的request文本

用浏览器作为客户端。

request由客户端程序发给服务器。

尽管request也可以像response那样分为三部分,request的格式与response的格式并不相同。

request由客户发送给服务器,比如下面是一个request:

GET /test.jpg HTTP/1.x
Accept: text/*

起始行可以分为三部分,第一部分为请求方法(request method),第二部分是URL,第三部分为HTTP版本。

request method可以有GET, PUT, POST, DELETE, HEAD。最常用的为GET和POST。

GET是请求服务器发送资源给客户,

POST是请求服务器接收客户送来的数据。

当我们打开一个网页时,我们通常是使用GET方法;

当我们填写表格并提交时,我们通常使用POST方法。

第二部分为URL,它通常指向一个资源(服务器上的资源或者其它地方的资源)。

像现在这样,就是指向当前服务器的当前目录的test.jpg。

按照HTTP协议的规定,服务器需要根据请求执行一定的操作。

正如我们在服务器程序中看到的,我们的Python程序先检查了request的方法,随后根据URL的不同,来生成不同的response(text_content或者pic_content)。

随后,这个response被发送回给客户端。

分析运行结果

在服务器终端,可以看到浏览器发出的第一个请求:

Request is: b’GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\n

服务器根据这个请求,发送给浏览器text_content的内容

浏览器接收到text_content之后,发现正文的html文本中有<IMG src="text.jpg" />,知道需要获得text.jpg文件来补充为图片,立即发出了第二个请求:

Request is: b’GET /test.jpg HTTP/1.1\r\nHost: 127.0.0.1:8000

服务器的Python程序分析过起始行之后,发现/test.jpg符合if条件,所以将pic_content发送给客户

Connected by ('127.0.0.1', 5186)
Request is: b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nsec-ch-ua: "Chromium";v="94", "Microsoft Edge";v="94", ";Not A Brand";v="99"\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: "Windows"\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.50\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n\r\n'
    
Connected by ('127.0.0.1', 8177)
Request is: b'GET /test.jpg HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nConnection: keep-alive\r\nsec-ch-ua: "Chromium";v="94", "Microsoft Edge";v="94", ";Not A Brand";v="99"\r\nsec-ch-ua-mobile: ?0\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.50\r\nsec-ch-ua-platform: "Windows"\r\nAccept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\nSec-Fetch-Site: same-origin\r\nSec-Fetch-Mode: no-cors\r\nSec-Fetch-Dest: image\r\nReferer: http://127.0.0.1:8000/\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n\r\n'
    
Connected by ('127.0.0.1', 6015)
Request is: b'GET /favicon.ico HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nConnection: keep-alive\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nsec-ch-ua: "Chromium";v="94", "Microsoft Edge";v="94", ";Not A Brand";v="99"\r\nsec-ch-ua-mobile: ?0\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.50\r\nsec-ch-ua-platform: "Windows"\r\nAccept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\nSec-Fetch-Site: same-origin\r\nSec-Fetch-Mode: no-cors\r\nSec-Fetch-Dest: image\r\nReferer: http://127.0.0.1:8000/\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n\r\n'

第三个请求 是因为浏览器默认都会去请求favicon.ico图标

favicon.ico 图标用于收藏夹图标和浏览器标签上的显示

上面的服务器程序中,用while循环来让服务器一直工作下去。实际上,还可以根据多线程的知识,将while循环中的内容改为多进程或者多线程工作。