客戶端/服務器架構也稱主從式架構,簡稱C/S架構,它是一種網絡結構,把客戶端(Client)(一般是一個採用圖形界面的程序)與服務器(server)區分開來,在C/S架構中,服務器是一系列的硬件或軟件,客戶端是提交服務請求的用戶,客戶端提供用戶請求接口,服務端響應請求進行對應的處理,並返回給客戶端。客戶端/服務器架構既能夠應用於計算機硬件,也能夠應用於軟件。linux
典型的硬件客戶端/服務器架構就是打印機,在企業中,員工經過局域網將我的電腦鏈接到打印機上,做爲客戶端向打印機發送打印請求,打印機做爲服務端完成響應處理相應的請求。程序員
軟件服務器也是運行在硬件之上的,典型的軟件服務器是Web服務器。在一臺或多臺電腦上搭建Web服務器,以提供用戶訪問所需的Web頁面和應用程序,Web服務器一旦啓動,都將可能永遠運行,除非受到一些外力驅使纔會中止,如人爲關閉,服務器硬件故障等。它的工做就是接收客戶端的請求,並響應請求給客戶端返回相應的Web頁面,而後等待下一個客戶端的請求。編程
套接字是網絡編程中的一個基本組件,若是想要服務器可以響應客戶端發來的請求,首先要創建一個通訊端點,使服務器可以監聽服務,當通訊端點創建後,就會進入無限循環的等待請求狀態,當接收到客戶端的請求,就會響應該請求。緩存
套接字就是兩個程序之間的信息通道,能夠理解爲上面提到的「通訊端點」的概念。在通訊開始以前,網絡應用程序必須建立套接字。套接字是網絡通訊過程當中端點的抽象表示,包含進行網絡通訊必須的五種信息:鏈接使用的協議,本地主機的IP地址,本地進程的協議端口號,遠程主機的IP地址,遠程進程的協議端口號。服務器
套接字起源於 20 世紀 70 年代,它是加利福尼亞大學伯克利分校版本的 Unix的一部分,即人們所說的 BSD Unix。 所以,套接字也被人們稱爲「伯克利套接字」或「BSD 套接字」。套接字最初被設計用於同一臺主機上多個應用程序之間的通信,這也就是所謂的進程間通信(IPC)。網絡
TCP用主機的IP地址加上主機的端口號做爲TCP鏈接的端點,這種端點就叫作套接字(socket)或插口。套接字用(IP地址:端口號)表示。架構
套接字有兩種類型,分別是基於文件的和基於網絡的。socket
基於文件的套接字家族名字叫作「AF_UNIX」,表明地址家族(address family):UNIX。在Unix和linux操做系統中,熟爲人知的一句話就是:一切皆文件,一個或多個進程運行在同一臺機器上,因此套接字是基於文件的,它就能夠經過訪問底層的基礎結構來實現進程之間的通訊tcp
基於網絡的套接字家族名字叫作「AF_INET」,表明地址家族(address family):INET(因特網)。它使用IPv4進行通訊,由於IPv4使用32位地址,相比於IPv6的128位來講,計算更快,更適合於局域網的通訊。目前它也是使用最普遍的。函數
在本文中,重點講網絡編程,因此在後面的涉及最多的仍是AF_INET。
不論使用哪一種地址家族,都只有兩種套接字的鏈接方式,一種是面向鏈接的,一種是無鏈接的。
面向鏈接的套接字鏈接方式,意味着在進程通訊以前必須先創建好一個鏈接,這種套接字就稱爲流式套接字。
流式套接字用於提供面向鏈接、可靠的數據傳輸服務。該服務將保證數據可以實現無差錯、無重複發送,並按順序接收。流式套接字之因此可以實現可靠的數據服務,緣由在於其使用了傳輸控制協議,即TCP(The Transmission Control Protocol)協議。在Python中,建立TCP套接字,就必須聲明SOCK_STREAM做爲套接字類型。
數據報套接字提供了一種無鏈接的服務。這也意味着,使用這種鏈接方式不須要在進程通訊前創建鏈接。在數據的傳輸過程當中,SOCK_DGRAM並不能保證數據傳輸的可靠性,數據有可能在傳輸過程當中丟失或出現數據重複,且沒法保證順序地接收到數據。數據報套接字使用UDP(User Datagram Protocol)協議進行數據的傳輸。因爲數據報套接字不能保證數據傳輸的可靠性,對於有可能出現的數據丟失狀況,須要在程序中作相應的處理。
雖然存在數據丟失、重複、數據無序接受等不少缺點,但它也有優點所在,在流式套接字中,由於是面向鏈接並提供了可靠的數據傳輸服務,這對於虛擬電路鏈接的維護須要很大的開銷,但數據報套接字就不須要這些額外的開銷,因此維護、資源佔用成本更低。
Python是一個很強大的網絡編程工具,Python內有不少針對網絡協議的庫,這些庫對網絡協議的各個層次進行抽象封裝,這對於程序員來講就意味着:沒必要關心網絡協議的原理,只須要經過對程序的邏輯處理,就能夠實現網絡數據的傳輸。
在Python中,建立套接字須要使用socket模塊,經過socket()函數建立套接字對象。
1 class socket(_socket.socket): 2 -- skip -- 3 def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None): 4 -- skip --
從socket函數的的構造方法中能夠看出,能夠指定地址家族和套接字的鏈接方式,proto默認是0,一般都省略。即建立套接字對象的時候:
import socket
#建立TCP/IP套接字,地址家族AF_INET
tcp_socket = socket.socket(socket.AF_INET,socket.SOCKET_STREAM)
#建立UDP/IP套接字,地址家族AF_INET
udp_socket = socket.socket(socket.AF_INET,socket.SOCKET_DGRAM)
常見的套接字內置函數
方法 |
功能 |
st.recv() |
接受TCP的消息 |
st.recv_into() |
接受TCP的消息到指定的緩存區 |
st.send()
|
發送TCP的消息(當待發送的消息量大於緩存區剩餘內存時,數據會丟失) |
st.sendall()
|
完整的發送TCP消息(當待發送的消息量大於緩存區剩餘內存時,數據不會丟失,循環調用send 直到發完爲止) |
st.recvfrom() |
接收UDP的消息 |
st.recvfrom_into() |
接收UDP的消息到指定的緩存區 |
st.sendto() |
發送UDP的消息 |
st.getpeername() |
鏈接到套接字的遠程地址(TCP) |
st.getsockname() |
獲取當前套接字的地址 |
st.getsockopt() |
獲取指定套接字的參數 |
st.setsockopt() |
設置指定套接字的參數 |
st.close() |
關閉套接字 |
st.shutdown() |
關閉鏈接 |
服務端套接字方法
方法 |
功能 |
st.bind() |
將IP地址+端口號綁定到套接字上 |
st.listen() |
開啓TCP監聽功能 |
st.accept() |
被動的接受TCP客戶端的鏈接,(阻塞式)一直等待鏈接直到鏈接到達 |
客戶端套接字方法
方法 |
功能 |
st.connect() |
主動發起TCP服務器鏈接 |
st.connect_ex() |
connect()的擴展版本,以錯誤代碼的形式返回問題,而不是拋出異常 |
面向阻塞的套接字方法
方法 |
功能 |
st.setblocking() |
設置套接字爲阻塞模式或非阻塞模式 |
st.settimeout() |
設置阻塞套接字的操做超時時間 |
st.gettimoout() |
獲取阻塞套接字的操做超時時間 |
面向文件的套接字方法
方法 |
功能 |
st.fileno() |
套接字的文件描述符 |
st.makefile() |
建立與套接字相關聯的文件對象 |
數據屬性
屬性 |
功能 |
st.family |
套接字家族 |
st.type |
套接字類型 |
st.proto |
套接字協議 |
上面提到過,套接字對象都是經過socket.socket()函數來建立的,下面模擬一個TCP服務器和客戶端,來實現進程間的通訊。
Tcp服務端:
1 import socket 2 tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) //建立服務器套接字 3 tcp_server.bind(("127.0.0.1",8000)) //將套接字與地址綁定 4 tcp_server.listen(5) //創建監聽鏈接 5 print("The server has started") 6 while True: 7 conn,addr= tcp_server.accept() //接受客戶端的鏈接 8 while True: 9 try: 10 data = conn.recv(1024) //會話的接收(或發送) 11 print("msg is",data.decode("utf-8")) //要將收到的會話數據進行解碼 12 conn.send(data.title()) //會話的發送(或接受) 13 except Exception: 14 break 15 conn.close() //關閉鏈接 16 tcp_server.close() //關閉服務器套接字
在Tcp服務端,先建立服務器套接字並指定類型爲流式套接字(SOCK_STREAM)。由於服務器須要佔用一個端口並等待客戶端的請求,因此它們必須綁定到一個本地地址。Tcp是一種面向鏈接的通訊方式,因此必須創建監聽鏈接,listen(5)的意義是容許傳入鏈接的最大數爲5個。當調用accept()函數後,服務端就會進入一個等待狀態,默認狀況下,accept()處於阻塞狀態,也就意味着,執行到此處,程序會暫停,直到有新的鏈接到達,纔會進行下一步的收發操做。
Tcp客戶端:
1 import socket 2 tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) //建立客戶端套接字 3 tcp_client.connect(("127.0.0.1",8000)) //鏈接服務器 4 while True: 5 msg = input("Please input your message:").strip() 6 if not msg:continue 7 tcp_client.send(msg.encode("utf-8")) //會話接收(或發送) 8 data = tcp_client.recv(1024) 9 print("reply is",data.decode("utf-8")) 10 tcp_client.close() //關閉客戶端套接字
建立客戶端比服務端要簡單不少,客戶端一旦擁有了套接字,就能夠利用套接字的connect()方法直接建立一個服務器的鏈接,創建好鏈接,就能夠參與到服務端的會話中,當客戶端的需求所有完成,就會關閉套接字,終止這次鏈接。
Udp服務器不須要Tcp服務器那麼多的配置,由於它不是面向鏈接的,除了等待傳入的鏈接,它基本不須要其餘的操做。
Udp服務端:
1 import socket 2 ip_port = ("127.0.0.1",8000) 3 udp_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) //建立服務端套接字 4 udp_server.bind(ip_port) //綁定本地地址 5 print("the server has started") 6 while True: 7 data,addr = udp_server.recvfrom(1024) //關閉接收(或發送) 8 print(data) 9 udp_server.sendto(data.title(),addr) //關閉發送(或接受) 10 從上面代碼中能夠看出,除了建立套接字並綁定本地地址後,基本
沒有其它的操做,它是無鏈接的,這也就意味着,它無需爲了成功通訊而使一個客戶端鏈接到一個「特定」的套接字進行轉換操做,服務器端僅僅是接收數據並進行回覆。
Udp客戶端:
1 import socket 2 ip_port = ("127.0.0.1",8000) 3 udp_client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) //建立服務端套接字 4 while True: 5 msg = input(">>>").strip() 6 udp_client.sendto(msg.encode("utf-8"),ip_port) //發送 7 data,addr = udp_client.recvfrom(1024) //接收 8 print(data) 9 udp_client.close() //關閉套接字
Udp客戶端,一旦建立了套接字,就能夠進行會話循環中,當會話結束,關閉套接字。
在使用Udp進行通訊的時候,服務端能夠同時接收多個客戶端的會話請求並返回請求結果。