python 服務器和客戶端通信


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循環中的內容改為多進程或者多線程工作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM