Python網(wǎng)絡(luò)編程 -- TCP/IP

首先放出一個(gè) TCP/IP 的程序,這里是單線程服務(wù)器與客戶端,在多線程一節(jié)會(huì)放上多線程的TCP/IP服務(wù)程序。

這里將服務(wù)端和客戶端放到同一個(gè)程序當(dāng)中,方便對(duì)比服務(wù)端與客戶端的不同。

import sys
import socket
import struct
from argparse import ArgumentParser

'''
確定發(fā)送數(shù)據(jù)的 長(zhǎng)度值 的編碼的長(zhǎng)度, 2*32 - 1, 最大數(shù)據(jù)表示為 4GB
'''
header_struct = struct.Struct('>I')

'''
按照定長(zhǎng)接收數(shù)據(jù)
如果長(zhǎng)度為0,則會(huì)跳過(guò)接收過(guò)程,返回空數(shù)據(jù),不會(huì)報(bào)錯(cuò)
如果接收數(shù)據(jù)長(zhǎng)度比length小,則報(bào)錯(cuò)。

'''
def recvall(sock, length):
    data = b''
    while len(data) < length:
        more = sock.recv(length - len(data))
        if not more:
            raise EOFError('Expected {} bytes but only received '
                           '{} bytes before the socket closed'\
                           .format(length, len(data)))
        data += more
    return data

'''
按照固定長(zhǎng)度前綴接收數(shù)據(jù),先接收固定長(zhǎng)度的數(shù)據(jù),表示數(shù)據(jù)長(zhǎng)度,然后按照長(zhǎng)度信息接收后續(xù)全部數(shù)據(jù)并返回
'''
def get_block(sock):
    data = recvall(sock, header_struct.size)
    (data_length, ) = header_struct.unpack(data)
    return recvall(sock, data_length)

'''
為數(shù)據(jù)添加固定字節(jié)長(zhǎng)度的 數(shù)據(jù)長(zhǎng)度信息并發(fā)送。message 為字節(jié)格式。
'''
def put_block(sock, message):
    length = len(message)
    sock.sendall(header_struct.pack(length))
    sock.sendall(message)
'''

創(chuàng)建服務(wù)器,接收 (ip, port) 作為輸入,循環(huán)接收所有可以接收的信息,返回固定內(nèi)容。
'''
def server(address):

    # 創(chuàng)建一個(gè) socket 對(duì)象
    # socket.AF_INET    --- family  地址族, AF_UNIX(只能用于單一的Unix系統(tǒng)進(jìn)程間通信), AF_INET, AF_INET6, 默認(rèn) AF_INET
    # socket.SOCK_STREAM   ---type  套接字類型, SOCK_STREAM (流式socket,用于TCP), SOCK_DGRAM (數(shù)據(jù)報(bào) socket, 用于 UDP), SOCK_RAW, SOCK_SEQPACKET) 默認(rèn) SOCK_STREAM
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # setsocketopt(sock, level, optname, optval, optlen),若是用 實(shí)例sock.setsocketopt,則忽略第一個(gè)參數(shù)sock。
    # level 指定控制套接字的層次,可以有  SOL_SOCKET:通用套接字;IPPROTO_IP: IP選項(xiàng);IPPROTO_TCP:TCP選項(xiàng)
    # optname 控制方式,每種控制層級(jí)下都有不同的控制方式,比方說(shuō) SOL_SOCKET 下的 SO_REUSEADDR:允許重用本地地址和端口
    # optval 設(shè)置上述控制方式的值
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 將socket 綁定到特定的 ip + 端口, 用于服務(wù)器對(duì)外的連接地址
    # 如果 ip 為空,表示 任意地址
    sock.bind(address)

    # listen 才決定程序用于服務(wù)器
    # listen()在TCP套接字上的調(diào)用,會(huì)徹底轉(zhuǎn)變?cè)撎捉幼值慕巧?,此時(shí)該套接字只能用于 accept(),不會(huì)與任何特定的客戶端連接
    # listen()調(diào)用對(duì)套接字的改變是無(wú)法撤銷的。
    sock.listen(5)
    print('Run this script in another window with "-c" to connect')
    print('Now Server is listening at ', sock.getsockname())
    while True:
        # 接受 客戶端連接,accept()會(huì)返回一個(gè)全新的套接字,該新建套接字負(fù)責(zé)管理對(duì)應(yīng)的新建會(huì)話。
        # sc 新創(chuàng)建的socket, 用于連接,連接套接字
        # addr 遠(yuǎn)端的地址,  ip + 端口
        sc, addr = sock.accept()
        print('Accepted connection from: ', addr)
        print('Connection socket: ', sc)
        # 此時(shí)可以調(diào)用 shutdown 功能,關(guān)閉某一方向上的連接。
        # SHUT_WR 表示 本連接后續(xù)只接收, 不發(fā)送。
        # SHUT_RD 表示本連接后續(xù)只發(fā)送,不接收
        # SHUT_RDWR 關(guān)閉兩個(gè)方向上的通信, 與 close()不同,
        # 如果操作系統(tǒng)允許多個(gè)程序共用一個(gè)套接字,那么close()只是關(guān)閉單個(gè)進(jìn)程對(duì)套接字的調(diào)用,其他進(jìn)程仍然可以使用該套接字,
        # 而 SHUT_RDWR 則是關(guān)閉套接字,所有的使用該套接字的進(jìn)程都不可用。
        # sc.shutdown(socket.SHUT_WR)
        while True:
            message = b''
            while True:
                more = get_block(sc)
                # 判斷more是否為空,
                # 因?yàn)榧s定每次發(fā)送數(shù)據(jù)最后都要發(fā)送一個(gè)空數(shù)據(jù)最為結(jié)尾,只要沒有接收到空數(shù)據(jù),則表示數(shù)據(jù)沒有完整接收
                if not more:
                    break
                message += more
            print('From client: ', repr(message))
            if message == 'exit'.encode('utf-8'):
                break
            message = b'Got your message, please continue.'
            sys.stdout.flush()  # 刷新輸出
            put_block(sc, message)
        sc.close()

def client(address):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(address)
    print('Now client connected to ', sock.getsockname())
    print('Please input the data you want to send, input "exit" to exit.')
    while True:
        data = input('client: ')
        message = data.encode('utf-8')
        put_block(sock, message)
        put_block(sock, b'')  # 最后發(fā)送一個(gè)空信息,表示數(shù)據(jù)結(jié)尾。
        if data == 'exit':
            sock.close()
            break
        message = get_block(sock)
        print('Server says: ', repr(message))
        sys.stdout.flush()

if __name__ == '__main__':
    choices = {'server': server, 'client': client}
    parser = ArgumentParser(description='Transmit and receive blocks over TCP')
    parser.add_argument('role', choices=choices, help='which role to play ?')
    parser.add_argument('host', default='127.0.0.1', help='interface the server listens at: '
                                                          'host the client sends to.')

    # metavar 用于顯示幫助信息時(shí)的顯示 , 不影響實(shí)際使用
    parser.add_argument('-p', type=int, metavar='PORT', default=1060,
                        help='TCP port (default 1060)')
    args = parser.parse_args()
    function=choices[args.role]
    function((args.host, int(args.p)))
  1. OSI (Open System Interconnect) 開放式系統(tǒng)互連,是ISO國(guó)際標(biāo)準(zhǔn)化組織在1985年提出的網(wǎng)絡(luò)互聯(lián)模型。OSI 通過(guò)分層的技術(shù),將網(wǎng)絡(luò)分為若干層,每一層實(shí)現(xiàn)不同的功能,每一層向上提供服務(wù),向下提供應(yīng)用,通過(guò)標(biāo)準(zhǔn)化的接口與協(xié)議,各司其職,完成整個(gè)通信過(guò)程。在實(shí)現(xiàn)通信的過(guò)程中,每一層(軟件或者設(shè)備,見下圖)與遠(yuǎn)端對(duì)等層進(jìn)行虛擬通信(具體實(shí)現(xiàn)需要逐層向下至最下層的物理層進(jìn)行連接),只需要考慮本層的協(xié)議與實(shí)現(xiàn)方法即可,稱之為水平協(xié)議,垂直服務(wù)。

TCP/IP是因特網(wǎng)的通信協(xié)議,其參考OSI模型,也采用了分層的方式,對(duì)每一層制定了相應(yīng)的標(biāo)準(zhǔn)。

  1. OSI 參考模型與TCP/IP模型


    OSI參考模型與TCP IP模型.png
  2. 網(wǎng)絡(luò)設(shè)備間進(jìn)行共享的基本單元是數(shù)據(jù)包 (packet),一個(gè)數(shù)據(jù)包是一串長(zhǎng)度在幾字節(jié)到幾千字節(jié)間的字節(jié)串,數(shù)據(jù)包在物理層通常只有兩個(gè)屬性:包含的字節(jié)串?dāng)?shù)據(jù)以及目標(biāo)傳輸?shù)刂?,網(wǎng)卡負(fù)責(zé)發(fā)送并接收這樣的數(shù)據(jù)包。

網(wǎng)際協(xié)議(IP)是為全世界通過(guò)互聯(lián)網(wǎng)連接的計(jì)算機(jī)賦予統(tǒng)一地址系統(tǒng)的機(jī)制,它使得數(shù)據(jù)包能夠從互聯(lián)網(wǎng)的一端發(fā)送至另一端,如 130.207.244.244,為了便于記憶,常用主機(jī)名代替IP地址,例如 baidu.com。

  1. UDP/TCP, IP協(xié)議只負(fù)責(zé)嘗試將每個(gè)數(shù)據(jù)包傳輸至正確的機(jī)器,但是兩個(gè)獨(dú)立的應(yīng)用程序要維護(hù)一個(gè)會(huì)話的話,還需要兩個(gè)額外的特性:
  • 需要為兩臺(tái)主機(jī)間傳送的大量數(shù)據(jù)包打上標(biāo)簽,這樣就可以將表示網(wǎng)頁(yè)的數(shù)據(jù)包和用于電子郵件的數(shù)據(jù)包以及其他應(yīng)用的數(shù)據(jù)包分開,這一過(guò)程叫做多路復(fù)用 (multiplexing)。

  • 對(duì)兩臺(tái)主機(jī)間獨(dú)立傳輸?shù)臄?shù)據(jù)包流發(fā)生的任何錯(cuò)誤,都需要進(jìn)行修復(fù)。而丟失的數(shù)據(jù)包也需要進(jìn)行重傳,直到將其成功發(fā)送至目的地址。另外如果數(shù)據(jù)到到達(dá)時(shí)順序錯(cuò)亂,則要將這些數(shù)據(jù)包重組回正確的順序。最后,要丟棄重復(fù)的數(shù)據(jù)包,以保證數(shù)據(jù)流中的信息沒有冗余。提供這些保證的特性叫做可靠傳輸(reliable transport)

UDP (User Datagram Protocol,用戶數(shù)據(jù)報(bào)協(xié)議) 解決了上述第一個(gè)問(wèn)題,通過(guò)端口號(hào)來(lái)實(shí)現(xiàn)了多路復(fù)用(用不同的端口區(qū)分不同的應(yīng)用程序)但是使用UDP協(xié)議的網(wǎng)絡(luò)程序需要自己處理丟包、重包和包的亂序問(wèn)題。

TCP (Transmission Control Protocol,傳輸控制協(xié)議) 解決了上述兩個(gè)問(wèn)題,同樣使用端口號(hào)實(shí)現(xiàn)了復(fù)用。

TCP 實(shí)現(xiàn)可靠連接的方法:

  • 每個(gè)TCP數(shù)據(jù)包都有一個(gè)序列號(hào),接收方通過(guò)該序列號(hào)將數(shù)據(jù)正確排序,并發(fā)現(xiàn)丟失的數(shù)據(jù)包,并請(qǐng)求進(jìn)行重傳。

  • TCP并不使用順序的整數(shù)作為序列號(hào),而是通過(guò)計(jì)數(shù)器記錄發(fā)送的字節(jié)數(shù)。例如 如果一個(gè)包含1024字節(jié)的數(shù)據(jù)包的序列號(hào)為7200,那么下一個(gè)數(shù)據(jù)包的序列號(hào)就是8224.

  • 初始序列號(hào)隨機(jī)選擇。

  • TCP無(wú)須等待響應(yīng)就能一口氣發(fā)送多個(gè)數(shù)據(jù)包,某一時(shí)刻同時(shí)傳輸?shù)臄?shù)據(jù)量叫做TCP窗口。

  • 接收方的TCP可以通過(guò)控制發(fā)送方的窗口大小來(lái)減緩或暫停連接,這叫做流量控制(flow control)

  1. Soket是對(duì)TCP/IP協(xié)議的封裝,Socket并不是協(xié)議,而是一個(gè)調(diào)用接口(API),socket 起源于Unix,Unix/Linux的基本哲學(xué)之一就是“一切接文件”,都可以用“打開open -> 讀寫 write/read -> 關(guān)閉close”模式來(lái)操作。socket是支持TCP/IP協(xié)議的網(wǎng)絡(luò)通信的基本操作單元,是網(wǎng)絡(luò)通信過(guò)程中端點(diǎn)的抽象表示,包含網(wǎng)絡(luò)通信必須的 五種信息:連接時(shí)使用的協(xié)議,本地主機(jī)的IP地址,本地進(jìn)程的協(xié)議端口,遠(yuǎn)端主機(jī)的IP地址,遠(yuǎn)端進(jìn)程的協(xié)議端口。

socket通信模型及 TCP 通信過(guò)程如下兩張圖。

[圖片上傳失敗...(image-6d947d-1610703914730)]

image.png

[圖片上傳失敗...(image-30b472-1610703914730)]

image.jpeg
  1. TCP套接字的含義:
  • TCP是面向連接的協(xié)議,所以就需要提前建立一個(gè)通道,由通信雙發(fā)都鎖定一個(gè) IP+port 的通信地址,在服務(wù)端就需要有一個(gè) bind,綁定一個(gè)特定的IP地址與端口號(hào),用于接收客戶端的連接,這個(gè)socket成為被動(dòng)套接字(passive socket), 又稱作 監(jiān)聽套接字 (listening socket)。服務(wù)器通過(guò)該套接字來(lái)接受連接請(qǐng)求,但是該套接字不能用于發(fā)送和接收任何數(shù)據(jù),也不表示任何的網(wǎng)絡(luò)會(huì)話。

  • 客戶端通過(guò) connect 去連接服務(wù)器,這是客戶端就有了一組 IP + Port,IP為本機(jī)地址,端口號(hào)默認(rèn)隨機(jī),這時(shí)服務(wù)器端 通過(guò) accept接收客戶端的連接,并創(chuàng)建一個(gè)新的socket 用戶通信。這一組 socket 稱為主動(dòng)套接字(active socket) 又叫作 連接套接字(connected socket),連接套接字只用于與該特定的遠(yuǎn)程主機(jī)進(jìn)行通信,看上去就好像是Unix系統(tǒng)的管道或文件,可以將TCP的連接套接字傳給另一個(gè)接收普通文件作為輸入的程序,而該程序永遠(yuǎn)也不會(huì)知道它其實(shí)正在進(jìn)行網(wǎng)絡(luò)通信。

  1. 套接字的5個(gè)坐標(biāo):

socket.getaddrinfo(host, port, family, socktype, proto, flags)
返回: [(family, socktype, proto, cannonname, sockaddr), ] 由元組組成的列表。
family:表示socket使用的協(xié)議簇, AF_UNIX : 1, AF_INET: 2, AF_INET6 : 10。 0 表示不指定。
socktype: socket 的類型, SOCK_STREAM : 1, SOCK_DGRAM : 2, SOCK_RAW : 3
proto: 協(xié)議, 套接字所用的協(xié)議,如果不指定, 則為 0。 IPPROTO_TCP : 6, IPPRTOTO_UDP : 17
flags:標(biāo)記,限制返回內(nèi)容。 AI_ADDRCONFIG 把計(jì)算機(jī)無(wú)法連接的所有地址都過(guò)濾掉(如果一個(gè)機(jī)構(gòu)既有IPv4,又有IPv6,而主機(jī)只有IPv4,則會(huì)把 IPv6過(guò)濾掉)
AI _V4MAPPED, 如果本機(jī)只有IPv6,服務(wù)卻只有IPv4,這個(gè)標(biāo)記會(huì)將 IPv4地址重新編碼為可實(shí)際使用的IPv6地址。
AI_CANONNAME,返回規(guī)范主機(jī)名:cannonname。
getaddrinfo(None, 'smtp', 0, socket.SOCK_STREAM, 0, socket.AP_PASSIVE)
getaddrinfo('ftp.kernel.org', 'ftp', 0, 'socket.SOCK_STREAM, 0, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED)
利用已經(jīng)通信的套接字名提供給getaddrinfo
mysock = server_sock.accept()
addr, port = mysock.getpeername()
getaddrinfo(addr, port, mysock.family, mysock.type, mysock.proto, socket.AI_CANONNAME)

'''
對(duì)服務(wù)器連接測(cè)試
提供任意網(wǎng)絡(luò)服務(wù)器的名字,得到能否連接的結(jié)果。
'''
import socket
import sys
import argparse

def connect_to(hostname_or_ip):
    try:
        # 利用 getaddrinfo 獲取套接字五元組
        # 然后根據(jù)這個(gè)五元組可以 創(chuàng)建 socket, connect 主機(jī)。
        infolist = socket.getaddrinfo(
            hostname_or_ip, 'www', 0, socket.SOCK_STREAM, 0,
            socket.AI_ADDRCONFIG | socket.AI_V4MAPPED | socket.AI_CANONNAME,
        )
    except socket.gaierror as e:
        print('Name service failure: ', e.args[1])
        sys.exit(1)
    info = infolist[0]      # getaddrinfo 返回所有可用的五元組,所以要先獲取其中一個(gè)元組
    socket_args = info[:3]  # 建立socket所需的三個(gè)元素 family, type, proto
    address = info[4]       # 地址元組(ip, port)
    s = socket.socket(*socket_args)
    try:
        s.connect(address)
    except socket.error as e:
        print('Network failure: ', e.args[1])
    else:
        print('Success: host ', info[3], ' is listening on prot 80')
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Try connecting to port 80')
    parser.add_argument('hostname', help='hostname that you want to contact')
    connect_to(parser.parse_args().hostname)

TCP 數(shù)據(jù)發(fā)送模式:

由于 TCP 是發(fā)送流式數(shù)據(jù),并且會(huì)自動(dòng)分割發(fā)送的數(shù)據(jù)包,而且在 recv 的時(shí)候會(huì)阻塞進(jìn)程,直到接收到數(shù)據(jù)為止,因此會(huì)出現(xiàn)死鎖現(xiàn)象,及通信雙方都在等待接收數(shù)據(jù)導(dǎo)致無(wú)法響應(yīng),或者都在發(fā)送數(shù)據(jù)導(dǎo)致緩存區(qū)溢出。所以就有了封幀(framing)的問(wèn)題,即如何分割消息,使得接收方能夠識(shí)別消息的開始與結(jié)束。

關(guān)于封幀,需要考慮的問(wèn)題是, 接收方何時(shí)最終停止調(diào)用recv才是安全的?整個(gè)消息或數(shù)據(jù)何時(shí)才能完整無(wú)缺的傳達(dá)?何時(shí)才能將接收到的消息作為一個(gè)整體來(lái)解析或處理。

  1. 半開連接,只涉及數(shù)據(jù)的發(fā)送,而不關(guān)注響應(yīng)。在這種情況下,可以使用這種模式:發(fā)送方循環(huán)發(fā)送數(shù)據(jù),直到所有數(shù)據(jù)都被傳遞給sendall為止,然后使用close()關(guān)閉套接字。接收方只需要不斷的調(diào)用recv(),直到recv()最后返回一個(gè)空字符串(表示發(fā)送方已經(jīng)關(guān)閉了套接字)為止。 sock.shutdown(socket.SHUT_WR)

  2. 模式一的變體,即在兩個(gè)方向上都通過(guò)流發(fā)送信息,首先通過(guò)流在一個(gè)方向上發(fā)送信息,然后關(guān)閉該方向,然后在另一個(gè)方向上發(fā)送信息,然后關(guān)閉套接字。

  3. 使用定長(zhǎng)消息,按照代碼中的 recvall 函數(shù),按照長(zhǎng)度接收消息。

  4. 通過(guò)某些方法,使用特殊字符劃分消息的邊界,如果發(fā)送的是ASCII字符串,可以選擇 '\0',或者 '\xff' 這樣處于ASCII 字符范圍之外的字符,然后循環(huán)接收消息直到收到邊界字符。只有消息使用的字母表有限時(shí),才能使用定界符機(jī)制。

  5. 在每個(gè)消息前加上其長(zhǎng)度作為前綴,消息長(zhǎng)度使用定長(zhǎng)的二進(jìn)制整數(shù),struct 模塊。 但是需要事先知道消息的長(zhǎng)度,如果事先無(wú)法得知消息的長(zhǎng)度,就不能用這個(gè)方法。

  6. 與模式5類似,但是不在是整個(gè)消息添加長(zhǎng)度前綴,而是將數(shù)據(jù)拆分成多個(gè)數(shù)據(jù)塊,每個(gè)數(shù)據(jù)塊之前都加上長(zhǎng)度前綴,抵達(dá)消息結(jié)尾時(shí),發(fā)送發(fā)可以發(fā)送一個(gè)與接收方事先約定好的信號(hào)(比如數(shù)字0表示的長(zhǎng)度字段),告知接收方,所有數(shù)據(jù)塊已經(jīng)發(fā)送完畢。

適用UDP的場(chǎng)景:

由于TCP每次連接與斷開都需要有三次握手,若有大量連接,則會(huì)產(chǎn)生大量的開銷,在客戶端與服務(wù)器之間不存在長(zhǎng)時(shí)間連接的情況下,適用UDP更為合適,尤其是客戶端太多的時(shí)候。

第二種情況: 當(dāng)丟包現(xiàn)象發(fā)生時(shí),如果應(yīng)用程序有比簡(jiǎn)單地重傳數(shù)據(jù)聰明得多的方法的話,那么就不適用TCP了。例如,如果正在進(jìn)行音頻通話,如果有1s的數(shù)據(jù)由于丟包而丟失了,那么只是簡(jiǎn)單地不斷重新發(fā)送這1s的數(shù)據(jù)直至其成功傳達(dá)是無(wú)濟(jì)于事的。反之,客戶端應(yīng)該從傳達(dá)的數(shù)據(jù)包中任意選擇一些組合成一段音頻(為了解決這一問(wèn)題,一個(gè)智能的音頻協(xié)議會(huì)用前一段音頻的高度壓縮版本作為數(shù)據(jù)包的開始部分,同樣將其后繼音頻壓縮,作為數(shù)據(jù)包的結(jié)束部分),然后繼續(xù)進(jìn)行后續(xù)操作,就好像沒有發(fā)生丟包一樣。如果使用TCP,那么這是不可能的,因?yàn)門CP會(huì)固執(zhí)地重傳丟失的信息,即使這些信息早已過(guò)時(shí)無(wú)用也不例外。UDP數(shù)據(jù)報(bào)通常是互聯(lián)網(wǎng)實(shí)時(shí)多媒體流的基礎(chǔ)。

參考資料:

  1. Python網(wǎng)絡(luò)編程(第3版) [美] Brandon Rhodes John Goerzen 著, 諸豪文 譯

  2. SOI參考模型和TCP/IP參考模: https://zhuanlan.zhihu.com/p/248667559

  3. Socket和Http通信原理:https://zhuanlan.zhihu.com/p/142650150

  4. socket通信模型:https://blog.csdn.net/Galen_xia/article/details/110876144

  5. setsockopt()函數(shù)功能介紹:https://www.cnblogs.com/eeexu123/p/5275783.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容