python 網絡編程 TCP/IP socket UDP

TCP/IP簡介


雖然你們如今對互聯網很熟悉,可是計算機網絡的出現比互聯網要早不少。html

計算機爲了聯網,就必須規定通訊協議,早期的計算機網絡,都是由各廠商本身規定一套協議,IBM、Apple和Microsoft都有各自的網絡協議,互不兼容,這就比如一羣人有的說英語,有的說中文,有的說德語,說同一種語言的人能夠交流,不一樣的語言之間就不行了。python

爲了把全世界的全部不一樣類型的計算機都鏈接起來,就必須規定一套全球通用的協議,爲了實現互聯網這個目標,互聯網協議簇(Internet Protocol Suite)就是通用協議標準。Internet是由inter和net兩個單詞組合起來的,原意就是鏈接「網絡」的網絡,有了Internet,任何私有網絡,只要支持這個協議,就能夠聯入互聯網。git

由於互聯網協議包含了上百種協議標準,可是最重要的兩個協議是TCP和IP協議,因此,你們把互聯網的協議簡稱TCP/IP協議。github

通訊的時候,雙方必須知道對方的標識,比如發郵件必須知道對方的郵件地址。互聯網上每一個計算機的惟一標識就是IP地址,相似123.123.123.123。若是一臺計算機同時接入到兩個或更多的網絡,好比路由器,它就會有兩個或多個IP地址,因此,IP地址對應的其實是計算機的網絡接口,一般是網卡。編程

IP協議負責把數據從一臺計算機經過網絡發送到另外一臺計算機。數據被分割成一小塊一小塊,而後經過IP包發送出去。因爲互聯網鏈路複雜,兩臺計算機之間常常有多條線路,所以,路由器就負責決定如何把一個IP包轉發出去。IP包的特色是按塊發送,途徑多個路由,但不保證能到達,也不保證順序到達。瀏覽器

internet-computers

TCP協議則是創建在IP協議之上的。TCP協議負責在兩臺計算機之間創建可靠鏈接,保證數據包按順序到達。TCP協議會經過握手創建鏈接,而後,對每一個IP包編號,確保對方按順序收到,若是包丟掉了,就自動重發。服務器

許多經常使用的更高級的協議都是創建在TCP協議基礎上的,好比用於瀏覽器的HTTP協議、發送郵件的SMTP協議等。網絡

一個IP包除了包含要傳輸的數據外,還包含源IP地址和目標IP地址,源端口和目標端口。多線程

端口有什麼做用?在兩臺計算機通訊時,只發IP地址是不夠的,由於同一臺計算機上跑着多個網絡程序。一個IP包來了以後,究竟是交給瀏覽器仍是QQ,就須要端口號來區分。每一個網絡程序都向操做系統申請惟一的端口號,這樣,兩個進程在兩臺計算機之間創建網絡鏈接就須要各自的IP地址和各自的端口號。app

一個進程也可能同時與多個計算機創建連接,所以它會申請不少端口。

瞭解了TCP/IP協議的基本概念,IP地址和端口的概念,咱們就能夠開始進行網絡編程了。

 

Python3 網絡編程

Python 提供了兩個級別訪問的網絡服務。:

  • 低級別的網絡服務支持基本的 Socket,它提供了標準的 BSD Sockets API,能夠訪問底層操做系統Socket接口的所有方法。
  • 高級別的網絡服務模塊 SocketServer, 它提供了服務器中心類,能夠簡化網絡服務器的開發。

什麼是 Socket?

Socket又稱"套接字",應用程序一般經過"套接字"向網絡發出請求或者應答網絡請求,使主機間或者一臺計算機上的進程間能夠通信。


socket()函數

Python 中,咱們用 socket()函數來建立套接字,語法格式以下:

socket.socket([family[, type[, proto]]])

參數

  • family: 套接字家族可使AF_UNIX或者AF_INET
  • type: 套接字類型能夠根據是面向鏈接的仍是非鏈接分爲SOCK_STREAMSOCK_DGRAM
  • protocol: 通常不填默認爲0.

Socket 對象(內建)方法

函數 描述
服務器端套接字
s.bind() 綁定地址(host,port)到套接字, 在AF_INET下,以元組(host,port)的形式表示地址。
s.listen() 開始TCP監聽。backlog指定在拒絕鏈接以前,操做系統能夠掛起的最大鏈接數量。該值至少爲1,大部分應用程序設爲5就能夠了。
s.accept() 被動接受TCP客戶端鏈接,(阻塞式)等待鏈接的到來
客戶端套接字
s.connect() 主動初始化TCP服務器鏈接,。通常address的格式爲元組(hostname,port),若是鏈接出錯,返回socket.error錯誤。
s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
公共用途的套接字函數
s.recv() 接收TCP數據,數據以字符串形式返回,bufsize指定要接收的最大數據量。flag提供有關消息的其餘信息,一般能夠忽略。
s.send() 發送TCP數據,將string中的數據發送到鏈接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。
s.sendall() 完整發送TCP數據,完整發送TCP數據。將string中的數據發送到鏈接的套接字,但在返回以前會嘗試發送全部數據。成功返回None,失敗則拋出異常。
s.recvform() 接收UDP數據,與recv()相似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。
s.sendto() 發送UDP數據,將數據發送到套接字,address是形式爲(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。
s.close() 關閉套接字
s.getpeername() 返回鏈接套接字的遠程地址。返回值一般是元組(ipaddr,port)。
s.getsockname() 返回套接字本身的地址。一般是一個元組(ipaddr,port)
s.setsockopt(level,optname,value) 設置給定套接字選項的值。
s.getsockopt(level,optname[.buflen]) 返回套接字選項的值。
s.settimeout(timeout) 設置套接字操做的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。通常,超時期應該在剛建立套接字時設置,由於它們可能用於鏈接的操做(如connect())
s.gettimeout() 返回當前超時期的值,單位是秒,若是沒有設置超時期,則返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 若是flag爲0,則將套接字設爲非阻塞模式,不然將套接字設爲阻塞模式(默認值)。非阻塞模式下,若是調用recv()沒有發現任何數據,或send()調用沒法當即發送數據,那麼將引發socket.error異常。
s.makefile() 建立一個與該套接字相關連的文件

簡單實例

服務端

咱們使用 socket 模塊的 socket 函數來建立一個 socket 對象。socket 對象能夠經過調用其餘函數來設置一個 socket 服務。

如今咱們能夠經過調用 bind(hostname, port) 函數來指定服務的 port(端口)

接着,咱們調用 socket 對象的 accept 方法。該方法等待客戶端的鏈接,並返回 connection 對象,表示已鏈接到客戶端。

完整代碼以下:

#!/usr/bin/python3
# 文件名:server.py

# 導入 socket、sys 模塊
import socket
import sys

# 建立 socket 對象
serversocket = socket.socket(
            socket.AF_INET, socket.SOCK_STREAM) 

# 獲取本地主機名
host = socket.gethostname()

port = 9999

# 綁定端口
serversocket.bind((host, port))

# 設置最大鏈接數,超事後排隊
serversocket.listen(5)

while True:
    # 創建客戶端鏈接
    clientsocket,addr = serversocket.accept()      

    print("鏈接地址: %s" % str(addr))
    
    msg='歡迎訪問菜鳥教程!'+ "\r\n"
    clientsocket.send(msg.encode('utf-8'))
    clientsocket.close()

  

客戶端

接下來咱們寫一個簡單的客戶端實例鏈接到以上建立的服務。端口號爲 12345。

socket.connect(hosname, port ) 方法打開一個 TCP 鏈接到主機爲 hostname 端口爲 port 的服務商。鏈接後咱們就能夠從服務端後期數據,記住,操做完成後須要關閉鏈接。

完整代碼以下:

#!/usr/bin/python3
# 文件名:client.py

# 導入 socket、sys 模塊
import socket
import sys

# 建立 socket 對象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 

# 獲取本地主機名
host = socket.gethostname() 

# 設置端口好
port = 9999

# 鏈接服務,指定主機和端口
s.connect((host, port))

# 接收小於 1024 字節的數據
msg = s.recv(1024)

s.close()

print (msg.decode('utf-8'))

  

如今咱們打開兩個終端,第一個終端執行 server.py 文件:

$ python3 server.py

第二個終端執行 client.py 文件:

$ python3 client.py 
歡迎訪問菜鳥教程!

這是咱們再打開第一個終端,就會看到有如下信息輸出:

鏈接地址: ('192.168.0.118', 33397)

Python Internet 模塊

如下列出了 Python 網絡編程的一些重要模塊:

協議 功能用處 端口號 Python 模塊
HTTP 網頁訪問 80 httplib, urllib, xmlrpclib
NNTP 閱讀和張貼新聞文章,俗稱爲"帖子" 119 nntplib
FTP 文件傳輸 20 ftplib, urllib
SMTP 發送郵件 25 smtplib
POP3 接收郵件 110 poplib
IMAP4 獲取郵件 143 imaplib
Telnet 命令行 23 telnetlib
Gopher 信息查找 70 gopherlib, urllib

 

TCP編程


Socket是網絡編程的一個抽象概念。一般咱們用一個Socket表示「打開了一個網絡連接」,而打開一個Socket須要知道目標計算機的IP地址和端口號,再指定協議類型便可。

客戶端

大多數鏈接都是可靠的TCP鏈接。建立TCP鏈接時,主動發起鏈接的叫客戶端,被動響應鏈接的叫服務器。

舉個例子,當咱們在瀏覽器中訪問新浪時,咱們本身的計算機就是客戶端,瀏覽器會主動向新浪的服務器發起鏈接。若是一切順利,新浪的服務器接受了咱們的鏈接,一個TCP鏈接就創建起來的,後面的通訊就是發送網頁內容了。

因此,咱們要建立一個基於TCP鏈接的Socket,能夠這樣作:

# 導入socket庫:
import socket
# 建立一個socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 創建鏈接:
s.connect(('www.sina.com.cn', 80))

  

建立Socket時,AF_INET指定使用IPv4協議,若是要用更先進的IPv6,就指定爲AF_INET6SOCK_STREAM指定使用面向流的TCP協議,這樣,一個Socket對象就建立成功,可是尚未創建鏈接。

客戶端要主動發起TCP鏈接,必須知道服務器的IP地址和端口號。新浪網站的IP地址能夠用域名www.sina.com.cn自動轉換到IP地址,可是怎麼知道新浪服務器的端口號呢?

答案是做爲服務器,提供什麼樣的服務,端口號就必須固定下來。因爲咱們想要訪問網頁,所以新浪提供網頁服務的服務器必須把端口號固定在80端口,由於80端口是Web服務的標準端口。其餘服務都有對應的標準端口號,例如SMTP服務是25端口,FTP服務是21端口,等等。端口號小於1024的是Internet標準服務的端口,端口號大於1024的,能夠任意使用。

所以,咱們鏈接新浪服務器的代碼以下:

s.connect(('www.sina.com.cn', 80))

注意參數是一個tuple,包含地址和端口號。

創建TCP鏈接後,咱們就能夠向新浪服務器發送請求,要求返回首頁的內容:

# 發送數據:
s.send('GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')

TCP鏈接建立的是雙向通道,雙方均可以同時給對方發數據。可是誰先發誰後發,怎麼協調,要根據具體的協議來決定。例如,HTTP協議規定客戶端必須先發請求給服務器,服務器收到後才發數據給客戶端。

發送的文本格式必須符合HTTP標準,若是格式沒問題,接下來就能夠接收新浪服務器返回的數據了:

# 接收數據:
buffer = []
while True:
    # 每次最多接收1k字節:
    d = s.recv(1024)
    if d:
        buffer.append(d)
    else:
        break
data = ''.join(buffer)

  

接收數據時,調用recv(max)方法,一次最多接收指定的字節數,所以,在一個while循環中反覆接收,直到recv()返回空數據,表示接收完畢,退出循環。

當咱們接收完數據後,調用close()方法關閉Socket,這樣,一次完整的網絡通訊就結束了:

# 關閉鏈接:
s.close()

接收到的數據包括HTTP頭和網頁自己,咱們只須要把HTTP頭和網頁分離一下,把HTTP頭打印出來,網頁內容保存到文件:

header, html = data.split('\r\n\r\n', 1)
print header
# 把接收的數據寫入文件:
with open('sina.html', 'wb') as f:
    f.write(html)

如今,只須要在瀏覽器中打開這個sina.html文件,就能夠看到新浪的首頁了。

服務器

和客戶端編程相比,服務器編程就要複雜一些。

服務器進程首先要綁定一個端口並監聽來自其餘客戶端的鏈接。若是某個客戶端鏈接過來了,服務器就與該客戶端創建Socket鏈接,隨後的通訊就靠這個Socket鏈接了。

因此,服務器會打開固定端口(好比80)監聽,每來一個客戶端鏈接,就建立該Socket鏈接。因爲服務器會有大量來自客戶端的鏈接,因此,服務器要可以區分一個Socket鏈接是和哪一個客戶端綁定的。一個Socket依賴4項:服務器地址、服務器端口、客戶端地址、客戶端端口來惟一肯定一個Socket。

可是服務器還須要同時響應多個客戶端的請求,因此,每一個鏈接都須要一個新的進程或者新的線程來處理,不然,服務器一次就只能服務一個客戶端了。

咱們來編寫一個簡單的服務器程序,它接收客戶端鏈接,把客戶端發過來的字符串加上Hello再發回去。

首先,建立一個基於IPv4和TCP協議的Socket:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

而後,咱們要綁定監聽的地址和端口。服務器可能有多塊網卡,能夠綁定到某一塊網卡的IP地址上,也能夠用0.0.0.0綁定到全部的網絡地址,還能夠用127.0.0.1綁定到本機地址。127.0.0.1是一個特殊的IP地址,表示本機地址,若是綁定到這個地址,客戶端必須同時在本機運行才能鏈接,也就是說,外部的計算機沒法鏈接進來。

端口號須要預先指定。由於咱們寫的這個服務不是標準服務,因此用9999這個端口號。請注意,小於1024的端口號必需要有管理員權限才能綁定:

# 監聽端口:
s.bind(('127.0.0.1', 9999))

緊接着,調用listen()方法開始監聽端口,傳入的參數指定等待鏈接的最大數量:

s.listen(5)
print 'Waiting for connection...'

接下來,服務器程序經過一個永久循環來接受來自客戶端的鏈接,accept()會等待並返回一個客戶端的鏈接:

while True:
    # 接受一個新鏈接:
    sock, addr = s.accept()
    # 建立新線程來處理TCP鏈接:
    t = threading.Thread(target=tcplink, args=(sock, addr))
    t.start()

每一個鏈接都必須建立新線程(或進程)來處理,不然,單線程在處理鏈接的過程當中,沒法接受其餘客戶端的鏈接:

def tcplink(sock, addr):
    print 'Accept new connection from %s:%s...' % addr
    sock.send('Welcome!')
    while True:
        data = sock.recv(1024)
        time.sleep(1)
        if data == 'exit' or not data:
            break
        sock.send('Hello, %s!' % data)
    sock.close()
    print 'Connection from %s:%s closed.' % addr

鏈接創建後,服務器首先發一條歡迎消息,而後等待客戶端數據,並加上Hello再發送給客戶端。若是客戶端發送了exit字符串,就直接關閉鏈接。

要測試這個服務器程序,咱們還須要編寫一個客戶端程序:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 創建鏈接:
s.connect(('127.0.0.1', 9999))
# 接收歡迎消息:
print s.recv(1024)
for data in ['Michael', 'Tracy', 'Sarah']:
    # 發送數據:
    s.send(data)
    print s.recv(1024)
s.send('exit')
s.close()

  

咱們須要打開兩個命令行窗口,一個運行服務器程序,另外一個運行客戶端程序,就能夠看到效果了:

client-server

須要注意的是,客戶端程序運行完畢就退出了,而服務器程序會永遠運行下去,必須按Ctrl+C退出程序。

小結

用TCP協議進行Socket編程在Python中十分簡單,對於客戶端,要主動鏈接服務器的IP和指定端口,對於服務器,要首先監聽指定端口,而後,對每個新的鏈接,建立一個線程或進程來處理。一般,服務器程序會無限運行下去。

同一個端口,被一個Socket綁定了之後,就不能被別的Socket綁定了。

UDP編程


TCP是創建可靠鏈接,而且通訊雙方均可以以流的形式發送數據。相對TCP,UDP則是面向無鏈接的協議。

使用UDP協議時,不須要創建鏈接,只須要知道對方的IP地址和端口號,就能夠直接發數據包。可是,能不能到達就不知道了。

雖然用UDP傳輸數據不可靠,但它的優勢是和TCP比,速度快,對於不要求可靠到達的數據,就可使用UDP協議。

咱們來看看如何經過UDP協議傳輸數據。和TCP相似,使用UDP的通訊雙方也分爲客戶端和服務器。服務器首先須要綁定端口:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 綁定端口:
s.bind(('127.0.0.1', 9999))

建立Socket時,SOCK_DGRAM指定了這個Socket的類型是UDP。綁定端口和TCP同樣,可是不須要調用listen()方法,而是直接接收來自任何客戶端的數據:

print 'Bind UDP on 9999...'
while True:
    # 接收數據:
    data, addr = s.recvfrom(1024)
    print 'Received from %s:%s.' % addr
    s.sendto('Hello, %s!' % data, addr)

recvfrom()方法返回數據和客戶端的地址與端口,這樣,服務器收到數據後,直接調用sendto()就能夠把數據用UDP發給客戶端。

注意這裏省掉了多線程,由於這個例子很簡單。

客戶端使用UDP時,首先仍然建立基於UDP的Socket,而後,不須要調用connect(),直接經過sendto()給服務器發數據:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in ['Michael', 'Tracy', 'Sarah']:
    # 發送數據:
    s.sendto(data, ('127.0.0.1', 9999))
    # 接收數據:
    print s.recv(1024)
s.close()

從服務器接收數據仍然調用recv()方法。

仍然用兩個命令行分別啓動服務器和客戶端測試,結果以下:

client-server

小結

UDP的使用與TCP相似,可是不須要創建鏈接。此外,服務器綁定UDP端口和TCP端口互不衝突,也就是說,UDP的9999端口與TCP的9999端口能夠各自綁定。

源碼參考:https://github.com/michaelliao/learn-python/tree/master/socket

相關文章
相關標籤/搜索