1. 程式人生 > >Python網路程式設計之socket應用

Python網路程式設計之socket應用

1 引言

2 網路基礎

3 socket介紹

4 socket基本使用

5 總結

1 引言

         本篇主要對Python下網路程式設計中用到的socket模組進行初步總結。首先從網路基礎理論出發,介紹了TCP協議和UDP協議;然後總結了socket中的常用函式;最後通過實際程式碼展示基本函式的應用。

2 網路基礎

  要想理解socket,首先得熟悉一下TCP/IP協議族。TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,定義了主機如何連入因特網及資料如何在它們之間傳輸的標準,從字面意思來看TCP/IP是TCP和IP協議的合稱,但實際上TCP/IP協議是指因特網整個TCP/IP協議族。下面對TCP/IP協議族中與socket緊密相關的的TCP協議和UDP協議進行介紹。

  TCP是流協議,而UDP是資料報協議。換句話說,TCP在客戶機和伺服器之間建立持續的開放連線,在該連線的生命期內,位元組可以通過該連線寫出(並且保證順序正確)。然而,通過 TCP 寫出的位元組沒有內建的結構,所以需要高層協議在被傳輸的位元組流內部分隔資料記錄和欄位。

  UDP是資料報協議,不需要在客戶機和伺服器之間建立連線,它只是在地址之間傳輸報文。UDP的一個很好特性在於它的包是自分隔的(self-delimiting),也就是一個數據報都準確地指出它的開始和結束位置。然而,UDP的一個可能的缺點在於,它不保證包將會按順序到達,甚至根本就不保證。不過,UDP有一個很大的有點就是效率高。

  TCP與UDP直接的對比就好似手機通訊和郵寄信件通訊。TCP猶如手機通訊機制,當呼叫者通過手機撥打接受者手機,只有接受者按下接聽鍵,兩方才算建立起了連線,且只要沒人結束通話,連線就一直是存活的,也只有在連線存活情況下兩者才能通話。UDP就如有郵局系統,只有有人來寄信,郵局就回幫他寄,但是不會去管收件人是否存在、也不管收件人什麼時候能收到信,如果寄件人陸續寄出多封信,收件人收的的信先後順序是混亂的,如果有的信沒有送達,那這封信就此消失在歷史的塵埃中。

  通常,人們用socket來建立計算機網路中多個主機(或程序)TCP/IP間的連線。

3 socket介紹

         Socket(中文譯為套接字)是作業系統核心中的一個數據結構,它幾乎是所有網路通訊的基礎。網路通訊,歸根到底還是程序間的通訊(不同計算機上的程序間通訊, 又稱為網路通訊, IP協議進行的主要是端到端通訊)。在網路中,每一個節點(計算機或路由)都有一個網路地址,也就是IP地址。兩個程序通訊時,首先要確定各自所在的網路節點的網路地址。但是,網路地址只能確定程序所在的計算機,而一臺計算機上很可能同時執行著多個程序,所以僅憑網路地址還不能確定到底是和網路中的哪一個程序進行通訊,因此套接字中還需要包括其他的資訊,也就是埠號(PORT)。在一臺計算機中,一個埠號一次只能分配給一個程序,也就是說,在一臺計算機中,埠號和程序之間是一 一對應的關係。

     socket使用(IP地址,協議,埠號)來標識一個程序。所以,使用埠號和網路地址的組合可以唯一的確定整個網路中的一個網路程序。埠號的範圍從0~65535,一類是由網際網路指派名字和號碼公司ICANN負責分配給一些常用的應用程式固定使用的“周知的埠”,其值一般為0~1023, 使用者自定義埠號一般大於等於1024。

網路上的兩個程式通過一個雙向的通訊連線實現資料的交換,這個連線的一端稱為一個socket。每一個socket都用一個半相關描述{協議、本地地址、本地埠}來表示;一個完整的套接字則用一個相關描述{協議、本地地址、本地埠、遠端地址、遠端埠}來表示。socket也有一個類似於開啟檔案的函式呼叫,該函式返回一個整型的socket描述符,隨後的連線建立、資料傳輸等操作都是通過socket描述符來實現的。以TCP協議中socket建立連線的“三次握手”為例,其過程如下:

 

  如圖所示,當客戶端通過socket呼叫connect時,觸發了連線請求,向伺服器傳送了SYN J包,這時connect進入阻塞狀態;伺服器監聽到連線請求,即收到SYN J包,呼叫socket的accept函式接收請求向客戶端傳送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到伺服器的SYN K ,ACK J+1之後,這時connect返回,並對SYN K進行確認;伺服器收到ACK K+1時,accept返回,至此三次握手完畢,連線建立。

      兩個socket通過“網路”互動資料進行資料互動時只負責兩件事:建立連線,傳遞資料;同時socket在收發資料時遵循的原則:有發就有收,收發必相等!

4 socket基本使用

1)socket函式

  功能:使用給定的地址族、套接字型別、協議編號(預設為0)來建立套接字。

  格式:socket.socket([family[, type[, proto]]])

  引數:

    family : AF_INET (預設ipv4),AF_INET6(ipv6) , AF_UNIX(Unix系統程序間通訊).

    type : SOCK_STREAM (TCP), SOCK_DGRAM(UDP) .

    protocol : 一般為0或者預設

  備註:如果socket建立失敗會丟擲一個socket.error異常

2)伺服器端函式

   a)bind函式

  格式:s.bind(address)

  功能:將地址address繫結到套接字, 地址以元組(host,port)的形式表示。

  引數:

    address為元組(host,port)

    host: ip地址, 為一個字串

    post: 自定義主機號, 為整型

b)listen函式

  格式:s.listen(backlog)

  功能:使伺服器的這個埠和IP處於監聽狀態,等待網路中某一客戶機的連線請求。如果客戶端有連線請求,埠就會接受這個連線。

  引數:backlog : 作業系統可以掛起的最大連線數量。該值至少為1,大部分應用程式設為5就可以了

c)accept函式

  格式:s.accept()

  功能:接受遠端計算機的連線請求,建立起與客戶機之間的通訊連線。伺服器處於監聽狀態時,如果某時刻獲得客戶機的連線請求,此時並不是立即處理這個請求,而是將這個請求放在等待佇列中,當系統空閒時再處理客戶機的連線請求。

  返回值:返回一個數組(conn,address),其中conn是新的套接字物件,可以用來接收和傳送資料。address是連線客戶端的地址

3)客戶端函式

  a)connect函式

  格式:s.connect(address)

  功能:用來請求連線遠端伺服器

  引數:address為遠端伺服器地址, 格式為元組(hostname,port),如果連接出錯,返回socket.error錯誤

  b)connect_ex函式

  格式:s.connect_ex(address)

  備註:connect()函式的擴充套件版本,出錯時返回出錯碼,而不是丟擲異常

4)通用函式

  a)recv函式

  格式:s.recv(bufsize[,flag])

  功能:接收遠端主機傳來的資料

  引數:

    bufsize : 指定要接收的資料大小

    flag : 提供有關訊息的其他資訊,通常可以忽略

  返回值:返回值為資料以字串形式</code>

  b)send函式

  格式:s.send(string[,flag])

  功能:傳送資料給指定的遠端主機

  引數:

    string : 要傳送的字串資料

    flag : 提供有關訊息的其他資訊,通常可以忽略

  返回值:返回值是要傳送的位元組數量,該數量可能小於string的位元組大小。

c)sendall函式

  格式:s.sendall(string[,flag])

  功能:內部呼叫了send函式,完整發送TCP資料。將string中的資料傳送到連線的套接字,但在返回之前會嘗試傳送所有資料。

  引數:同send函式

  返回值 : 成功返回None,失敗則丟擲異常。

d)close函式

  格式:s.close()

  功能:關閉套接字

e)recvfrom函式

  格式:s.recvfrom(bufsize[.flag])

  功能:與recv()類似,區別是返回值不同

  返回值:返回一個數組(data,address),其中data是包含接收資料的字串,address是傳送資料的套接字地址。

f)sendto函式

  格式:s.sendto(string[,flag],address)

  功能:將資料傳送到套接字

  引數:

    string : 要傳送的字串資料

    flag : 提供有關訊息的其他資訊,通常可以忽略

    address是形式為(ipaddr,port)的元組,指定遠端地址

  返回值:返回值是要傳送的位元組數量

  備註:該函式主要用於UDP協議。

g)settimeout函式

  格式:s.settimeout(timeout)

  功能:設定套接字操作的超時期

  引數:timeout是一個浮點數,單位是秒。值為None表示沒有超時期。一般,超時期應該在剛建立套接字時設定,因為它們可能用於連線的操作(如 client 連線最多等待5s )

h)getpeername函式

  格式:s.getpeername()

  功能:獲取連線套接字的遠端地址

  返回值:返回值通常是元組(ipaddr,port)。

i)getsockname函式

  格式:s.getsockname()

  功能:獲取套接字自己的地址

  返回值:通常是一個元組(ipaddr,port)

  socket中常用的函式就上面這些了。先用上面這些函式嘗試TCP協議下的socket通訊。

  伺服器端程式碼如下:

import socket

sk = socket.socket()


sk.bind(('127.0.0.1' ,8088))

sk.listen(5)

print('正在等待客戶端連線……')

conn , addr = sk.accept()

print('客戶端已連線到伺服器……')

mes_from_client = conn.recv(1024).decode('utf-8')

print(mes_from_client)

mes_to_server = '你好,客戶端,已收到你的資訊!'.encode('utf-8')#傳送的資料必須是byte型別

conn.send(mes_to_server)

conn.close()

sk.close()

  客戶端程式碼:

import socket

sk = socket.socket()

sk.connect(('127.0.0.1',8088))

mes_to_server = '你好,伺服器!'.encode('utf-8')#傳送的資料必須是byte型別

sk.send(mes_to_server)

mes_from_server = sk.recv(1024).decode('utf-8')

print(mes_from_server)

sk.close()

  注意:上述兩程式碼塊必須放在兩不同的py檔案中,且必須先執行伺服器程式碼,然後在開啟客戶端。開啟伺服器後,首先輸出“正在等待客戶端連線……”,然後程序會阻塞在accept函式中,下面的程式碼不會被執行,知道有客戶端連線過來。開啟客戶端後,伺服器端會先收到客戶端發來的資訊,然後客戶端也會受到伺服器發來的資訊。

  上面的例子中,伺服器和客戶端都是收發了一條資訊後socket關閉,如果要保持連線進行長時間通訊呢?那麼,我們可以把收發函式放入一個“while True”迴圈中:

  伺服器端程式碼:

import socket

 

BUF_SIZE = 1024  #設定緩衝區大小

server_addr = ('127.0.0.1', 8089)  #IP和埠構成表示地址

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  #生成一個新的socket物件

server.bind(server_addr)  #繫結地址

print("socket與地址繫結完成……")

server.listen(5)  #監聽, 最大監聽數為5

print("socket監聽開始……")

client, client_addr = server.accept()  #接收TCP連線, 並返回新的套接字和地址, 阻塞函式

print("報告:有客戶端請求連線,正在連線……")

print('客戶端地址為:{}'.format( client_addr))

while True :

    mes_from_client = client.recv(BUF_SIZE)  #從客戶端接收資料

    mes = mes_from_client.decode('utf-8')

    print('客戶端說:{}'.format(mes))

    mes = input('回覆客戶端的資訊>')

    mes_to_client = mes.encode('utf-8')

    client.sendall(mes_to_client)  #傳送資料到客戶端

server.close()

  客戶端程式碼:

import socket

BUF_SIZE = 1024  #設定緩衝區的大小

server_addr = ('127.0.0.1', 8089)  #IP和埠構成的伺服器地址

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  #返回新的socket物件

client.connect(server_addr)  #連線伺服器

while True:

    mes = input("傳送給伺服器的資訊> ")

    mes_to_server = mes.encode('utf-8')

    client.sendall(mes_to_server)  #傳送資料到伺服器

    mes_from_server = client.recv(BUF_SIZE)  #從伺服器端接收資料

    mes = mes_from_server.decode('utf-8')

    print(mes)

client.close()

  執行上述程式碼後,客戶端和伺服器可以長時間維持通訊。不過,使用socket時一定要注意,有發才有收,收發必相等,否則,就回出現異常。如果需要求換其他客戶端與當前伺服器進行通訊,必須先斷開當前客戶端的連線。

  再來嘗試UDP協議下socket通訊:

  伺服器端程式碼:

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)

sk.bind(('127.0.0.1' , 8090))

print('等待客戶端發來訊息……')

msg , addr = sk.recvfrom(1024) # 此處會阻塞

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

mes_to_server = '你好,客戶端,已收到你的資訊!'.encode('utf-8')#傳送的資料必須是byte型別

sk.sendto(mes_to_server,addr)

sk.close()

  客戶端程式碼:

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)

ip_port = ('127.0.0.1' , 8090)

mes_to_server = '你好,伺服器!'.encode('utf-8')#傳送的資料必須是byte型別

sk.sendto(mes_to_server , ip_port)

ret , addr = sk.recvfrom(1024)

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

sk.close()

  如果需要不停收發訊息,程式碼更改如下:

  伺服器端程式碼:

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)

sk.bind(('127.0.0.1' , 8090))

print('等待客戶端發來訊息……')

while True:

    msg , addr = sk.recvfrom(1024) # 此處會阻塞

    print('收到{}發來的資訊,內容是:{}'.format(addr , msg.decode('utf-8')))

    mes_to_server = input('>>>').encode('utf-8')#傳送的資料必須是byte型別

    sk.sendto(mes_to_server,addr)

sk.close()

  客戶端程式碼:

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)

ip_port = ('127.0.0.1' , 8090)

while True:

    mes_to_server = input('>>>').encode('utf-8')#傳送的資料必須是byte型別

    sk.sendto(mes_to_server , ip_port)

    ret , addr = sk.recvfrom(1024)

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

sk.close()

  使用socket進行UDP協議下通訊時,可以多個客戶端與伺服器通訊,也就是說,上面客戶端程式碼你可以另開一個程序與伺服器通訊,且不需要關閉當前客戶端,這是TCP協議與UDP協議下socket通訊的一個不同之處。

5 總結

  本篇初步總結了Python網路程式設計中socket模組的使用,事實上只是大致總結了基本函式的用法。其中諸多內容借鑑了一下兩篇部落格,感謝兩位博主。

  參考資料:

  https://www.cnblogs.com/maociping/p/5112019.html

  https://www.cnblogs.com/zhangyux/p/6109284.html