Python中TCP協(xié)議的理解

Num01-->TCP通信模型

這里寫圖片描述

Test01-->TCP客戶端案例

#! /usr/bin/env python3
# -*- coding:utf-8 -*- 

from socket import *

def main():

    # 1.創(chuàng)建socket
    client_socket = socket(AF_INET, SOCK_STREAM)

    # 2.指定服務器的地址和端口號
    server_addr = ('192.168.105.125',8080)
    client_socket.connect(server_addr)

    print('connect %s success' % str(server_addr))

    while True:
        # 3.給用戶提示,讓用戶輸入要檢索的資料
        send_data = input('>>')
        # 退出
        if send_data == 'quit':
            break
        # 向服務器請求數據
        client_socket.send(send_data.encode())

    client_socket.close()

if __name__ == "__main__":
    main()

Test02-->TCP服務器端案例

TCP服務器端創(chuàng)建流程如下:

1,socket創(chuàng)建一個套接字
2,bind綁定ip和port
3,listen使套接字變?yōu)榭梢员粍渔溄?br> 4,accept等待客戶端的鏈接
5,recv/send接收/發(fā)送數據

#! /usr/bin/env python3
# -*- coding:utf-8 -*- 

from socket import *
import time

def main():

    # 1.創(chuàng)建socket,stream流式套接字,對應tcp
    listen_socket = socket(AF_INET, SOCK_STREAM) 
    
    # 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
    #  設置socket層屬性    復用地址    允許
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.綁定端口號
    my_addr = ('192.168.105.125', 8080)
    #shift + insert
    listen_socket.bind(my_addr)

    # 3.接聽狀態(tài)
    #listen中的black表示已經建立鏈接和半鏈接的總數
    #如果當前已建立鏈接數和半鏈接數以達到設定值,那么新客戶端就不會connect成功,而是等待服務器。直到有鏈接退出。
    listen_socket.listen(4)
    print('listening...')

    # 4.等待客戶端來請求服務器

    while True:

        # 接受連接請求,創(chuàng)建新的連接套接字,用于客戶端連通信
        connect_socket, client_addr = listen_socket.accept() 
        # accept默認會引起阻塞 
        # 新創(chuàng)建連接用的socket, 客戶端的地址
        # print(connect_socket)
        # print(client_addr)

        while True:
            # tcp recv() 只會返回接收到的數據
            # 1024表示接受的數據長度
            recv_data = connect_socket.recv(1024)
            
            if len(recv_data) == 0:
                #發(fā)送方關閉tcp的連接,recv()不會阻塞,而是直接返回''
                print('client %s close' % str(client_addr))

                time.sleep(5)
                break

            print('recv: %s' % recv_data.decode('gbk'))
        
        # 用完之后,關閉新創(chuàng)建的那個connect_socket
        connect_socket.close()

if __name__ == "__main__":
    main()

Num02-->TCP協(xié)議三次握手

這里寫圖片描述

Num03-->TCP協(xié)議四次揮手

這里寫圖片描述

Num04-->TCP協(xié)議十種狀態(tài)

這里寫圖片描述

當一端收到一個FIN,內核讓read返回0來通知應用層另一端已經終止了向本端的數據傳送

發(fā)送FIN通常是應用層對socket進行關閉的結果

Num05-->TCP協(xié)議的2MSL問題

這里寫圖片描述

加以說明:
1,2MSL即兩倍的MSL,TCP的TIME_WAIT狀態(tài)也稱為2MSL等待狀態(tài)。

2,當TCP的一端發(fā)起主動關閉,在發(fā)出最后一個ACK包后,

3,即第3次握 手完成后發(fā)送了第四次握手的ACK包后就進入了TIME_WAIT狀態(tài),

4,必須在此狀態(tài)上停留兩倍的MSL時間,

5,等待2MSL時間主要目的是怕最后一個 ACK包對方沒收到,

6,那么對方在超時后將重發(fā)第三次握手的FIN包,

7,主動關閉端接到重發(fā)的FIN包后可以再發(fā)一個ACK應答包。

8,在TIME_WAIT狀態(tài) 時兩端的端口不能使用,要等到2MSL時間結束才可繼續(xù)使用。

9,當連接處于2MSL等待階段時任何遲到的報文段都將被丟棄。

10,不過在實際應用中可以通過設置 SO_REUSEADDR選項達到不必等待2MSL時間結束再使用此端口。

Num06-->TCP協(xié)議長鏈接和短鏈接

TCP在真正的讀寫操作之前,server與client之間必須建立一個連接,
當讀寫操作完成后,雙方不再需要這個連接時它們可以釋放這個連接,
連接的建立通過三次握手,釋放則需要四次握手,
所以說每個連接的建立都是需要資源消耗和時間消耗的。

Test01-->長鏈接

1, client 向 server 發(fā)起連接
2,server 接到請求,雙方建立連接
3,client 向 server 發(fā)送消息
4,server 回應 client
5,一次讀寫完成,連接不關閉
6,后續(xù)讀寫操作...
7,長時間操作之后client發(fā)起關閉請求

Test02-->短鏈接

1,client 向 server 發(fā)起連接請求
2,server 接到請求,雙方建立連接
3,client 向 server 發(fā)送消息
4,server 回應 client
5,一次讀寫完成,此時雙方任何一個都可以發(fā)起 close 操作

Test03-->長鏈接和短鏈接的區(qū)別

長鏈接可以省去較多的TCP建立和關閉的操作,減少浪費,節(jié)約時間。
對于頻繁請求資源的客戶來說,較適用長連接。

client與server之間的連接如果一直不關閉的話,會存在一個問題,
隨著客戶端連接越來越多,server早晚有扛不住的時候,這時候server端需要采取一些策略,
如關閉一些長時間沒有讀寫事件發(fā)生的連接,這樣可以避免一些惡意連接導致server端服務受損;

如果條件再允許就可以以客戶端機器為顆粒度,限制每個客戶端的最大長連接數,這樣可以完全避免某個蛋疼的客戶端連累后端服務。

短鏈接對于服務器來說管理較為簡單,存在的連接都是有用的連接,不需要額外的控制手段。但如果客戶請求頻繁,將在TCP的建立和關閉操作上浪費時間和帶寬。

Test04-->TCP長/短鏈接的應用場景

長鏈接多用于操作頻繁,點對點的通訊,而且連接數不能太多情況。
每個TCP連接都需要三次握手,這需要時間,如果每個操作都是先連接,
再操作的話那么處理速度會降低很多,所以每個操作完后都不斷開,
再次處理時直接發(fā)送數據包就OK了,不用建立TCP連接。

例如:數據庫的連接用長連接,如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創(chuàng)建也是對資源的浪費。

像WEB網站的HTTP服務一般都用短鏈接,因為長連接對于服務端來說會耗費一定的資源。

像WEB網站這么頻繁的成千上萬甚至上億客戶端的連接,用短連接會更省一些資源;如果用長連接,而且同時有成千上萬的用戶,如果每個用戶都占用一個連接的話,那可想而知吧。雖然并發(fā)量大,但每個用戶無需頻繁操作情況下需用短連好。

Num07-->TCP并發(fā)服務器--多進程實現

通過為每個客戶端創(chuàng)建一個進程的方式,能夠同時為多個客戶端進行服務。當客戶端不是特別多的時候,這種方式還行,如果有幾百上千個,就不可取了,因為每次創(chuàng)建進程等過程需要好較大的資源。

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke

from multiprocessing import Process
from socket import *


# 需要為客戶端提供服務
def do_service(connect_socket):
    while True:
        recv_data = connect_socket.recv(1024)
        if len(recv_data) == 0:
            # 發(fā)送方關閉tcp的連接,recv()不會阻塞,而是直接返回''
            # print('client %s close' % str(client_addr))     
            # s.getpeername()   s.getsockname()
            print('client %s close' % str(connect_socket.getpeername()))
            break
        print('recv: %s' % recv_data.decode('gbk'))


def main():
    # 1.創(chuàng)建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)
    # stream流式套接字,對應tcp

    # 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
    # 設置socket層屬性    復用地址,不用等2msl,    允許
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.綁定端口
    my_addr = ('192.168.105.125', 8080)
    listen_socket.bind(my_addr)

    # 3,接聽狀態(tài)
    listen_socket.listen(4)  # 設置套接字成監(jiān)聽,4表示一個己連接隊列長度
    print('listening...')

    # 4.等待客戶端來請求

    # 父進程只專注接受連接請求
    while True:
        # 接受連接請求,創(chuàng)建連接套接字,用于客戶端間通信
        connect_socket, client_addr = listen_socket.accept()  # accept默認會引起阻塞
        # 新創(chuàng)建連接用的socket, 客戶端的地址
        # print(connect_socket)
        print(client_addr)

        # 每當來新的客戶端連接,創(chuàng)建子進程,由子進程和客戶端通信
        process_do_service = Process(target=do_service, args=(connect_socket,))
        process_do_service.start()

        # 父進程,關閉connect_socket
        connect_socket.close()


if __name__ == "__main__":
    main()

Num08-->TCP并發(fā)服務器--多線程實現

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke
from socket import *
from threading import Thread


# 需要為客戶端提供服務
def do_service(connect_socket):
    while True:
        recv_data = connect_socket.recv(1024)
        if len(recv_data) == 0:
            # 發(fā)送方關閉tcp的連接,recv()不會阻塞,而是直接返回''
            # print('client %s close' % str(client_addr))
            # s.getpeername()   s.getsockname()
            print('client %s close' % str(connect_socket.getpeername()))
            break
        print('recv: %s' % recv_data.decode('gbk'))


def main():
    # 1.創(chuàng)建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)
    # 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
    # 設置socket層屬性    復用地址,不用等2msl    允許
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.綁定端口
    my_addr = ('192.168.105.125', 8080)
    # shift + insert
    listen_socket.bind(my_addr)

    # 3.接聽狀態(tài)
    listen_socket.listen(4)  # 設置套接字成監(jiān)聽,4表示一個己連接隊列長度
    print('listening...')

    # 4.等待來電話

    # 主線程只專注接受連接請求
    while True:
        # 接受連接請求,創(chuàng)建連接套接字,用于客戶端連通信
        connect_socket, client_addr = listen_socket.accept()  # accept默認會引起阻塞
        # 新創(chuàng)建連接用的socket, 客戶端的地址
        # print(connect_socket)
        print(client_addr)

        # 每當來新的客戶端連接,創(chuàng)建子線程,由子線程和客戶端通信
        thread_do_service = Thread(target=do_service, args=(connect_socket,))
        thread_do_service.start()

        # 主線程,不能關閉connect_socket,多個線程共享打開的文件
        # connect_socket.close()


if __name__ == "__main__":
    main()

Num09-->TCP單進程阻塞服務器實現

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke
import time
from socket import *


def main():
    # 1.創(chuàng)建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)

    # 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
    #  設置socket層屬性    復用地址    允許
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.綁定端口
    my_addr = ('192.168.105.125', 8080)
    listen_socket.bind(my_addr)

    # 3.接聽狀態(tài)
    listen_socket.listen(4)
    print('listening...')

    # 4.等待客戶端發(fā)起請求
    while True:

        # 接受連接請求,創(chuàng)建連接套接字,用于客戶端間通信
        connect_socket, client_addr = listen_socket.accept()  # accept默認會引起阻塞
        # 新創(chuàng)建連接用的socket, 客戶端的地址
        # print(connect_socket)
        print(client_addr)

        while True:
            # tcp recv() 只會返回接收到的數據
            recv_data = connect_socket.recv(1024)
            if len(recv_data) == 0:
                # 發(fā)送方關閉tcp的連接,recv()不會阻塞,而是直接返回''
                print('client %s close' % str(client_addr))
                time.sleep(5)
                break
            print('recv: %s' % recv_data.decode('gbk'))
        # 用完之后,關閉connect_socket
        connect_socket.close()


if __name__ == "__main__":
    main()

Num10-->TCP單進程非阻塞服務器實現

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke
import time
from socket import *


def main():
    # 1.創(chuàng)建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)
    # 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
    # 設置socket層屬性    復用地址    允許
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    # 設置listen_socket為非阻塞方式
    listen_socket.setblocking(False)  # 設置非阻塞

    # 2.綁定端口
    my_addr = ('192.168.105.125', 8080)
    listen_socket.bind(my_addr)

    # 3.接聽狀態(tài)
    listen_socket.listen(4)
    print('listening...')

    # 4.等待客戶端發(fā)起請求
    # 創(chuàng)建一個列表,保存已連接socket
    connect_socket_list = []
    while True:
        print(connect_socket_list)
        try:
            # 接受連接請求,創(chuàng)建連接套接字,用于客戶端連通信
            connect_socket, client_addr = listen_socket.accept()  # accept默認會引起阻塞 
        except Exception as e:  # 還沒有客戶端連接
            # print(e)
            # time.sleep(1)
            pass
        else:  # 此時有連接請求
            # 新創(chuàng)建連接用的socket, 客戶端的地址
            print('有新的客戶端連接 %s' % str(client_addr))
            # 將新socket 設成非阻塞
            connect_socket.setblocking(False)
            # 將新的socket添加到列表中,以便后續(xù)循環(huán)讀數據
            connect_socket_list.append(connect_socket)
        # 保存刪除socket列表
        need_delete_socket_list = []

        # 遍歷已連接的socket分別讀數據
        for new_socket in connect_socket_list:
            try:
                recv_data = new_socket.recv(1024)
            except:
                pass
            else:
                # 如果對方關閉
                if len(recv_data) == 0:
                    print('%s close' % (str(new_socket.getpeername())))
                    new_socket.close()
                    # 從connect_socket_list列表中刪除,單獨使用列表保存要刪除socket
                    need_delete_socket_list.append(new_socket)
                    continue
                print('from %s : %s' %
                      (str(new_socket.getpeername()), recv_data.decode('gbk')))
        # 從connect_socket_list刪除已關閉soccket
        for s in need_delete_socket_list:
            connect_socket_list.remove(s)
        time.sleep(1)


if __name__ == "__main__":
    main()

Num11-->select版--TCP服務器實現

Test01-->select 原理

在多路復用的模型中,比較常用的有select模型和epoll模型。這兩個都是系統(tǒng)接口,由操作系統(tǒng)提供。當然,Python的select模塊進行了更高級的封裝。

網絡通信被Unix系統(tǒng)抽象為文件的讀寫,通常是一個設備,由設備驅動程序提供,驅動可以知道自身的數據是否可用。支持阻塞操作的設備驅動通常會實現一組自身的等待隊列,如讀/寫等待隊列用于支持上層(用戶層)所需的block或non-block操作。設備的文件的資源如果可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到數據到來可用的時候,再喚醒進程。

這些設備的文件描述符被放在一個數組中,然后select調用的時候遍歷這個數組,如果對于文件描述符可讀則會返回該文件描述符。當遍歷結束之后,如果仍然沒有一個可用設備文件描述符,select則讓用戶進程睡眠,直到等待資源可用的時候再喚醒,喚醒之后遍歷之前那個監(jiān)視的數組。每次遍歷都是依次進行判斷的。

Test02-->select的優(yōu)缺點

優(yōu)點:select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優(yōu)點。
缺點:select的一個缺點在于單個進程能夠監(jiān)視的文件描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。

一般來說這個數目和系統(tǒng)內存關系很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.

對socket進行掃描時是依次掃描的,即采用輪詢的方法,效率較低。

當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。

Test03-->案例的實現代碼

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke
import select
import sys  # sys.stdin 代表鍵盤設備的文件對象
from socket import *
def main():
    # 1.創(chuàng)建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)
    # 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
    # 設置socket層屬性    復用地址    允許
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.綁定端口
    my_addr = ('192.168.105.125', 8080)
    listen_socket.bind(my_addr)

    # 3.接聽狀態(tài)
    listen_socket.listen(4)
    print('listening...')

    # 指定select關心的哪些路(socket,或文件)數據
    rlist = [listen_socket, sys.stdin]  # 要讀的文件對象列表,包括listen socket
    wlist = []  # 要寫的文件對象列表
    xlist = []  # 出現異常的文件對象列表

    while True:
        print(rlist)
        # select 會阻塞等待三個列表中文件對象就緒,如果沒就緒,select一直阻塞;只有任意文件就緒,select返回
        # 就緒的文件列表
        read_ready_list, wready, excplist = select.select(rlist, wlist, xlist)
        # 指定seclet關注讀,寫,異常文件列表

        # 如果select返回,一定有客戶端連接服務器
        # 循環(huán)判斷是哪個關注的文件,讀就緒了
        for fobj in read_ready_list:
            # 如果fobj是listen_socket對象,一定有客戶端連接服務器
            if fobj == listen_socket:
                new_socket, peer_addr = fobj.accept()  # 此時accept調用一定不會阻塞
                print(peer_addr)

                # 將新的socket添加至rlist,也要進行關注
                rlist.append(new_socket)

            elif fobj == sys.stdin:  # 鍵盤有數據輸入

                data = sys.stdin.readline()  # input()

                print('input %s' % data)

                if data == 'quit\n':
                    exit()

            else:  # 已連接socket有數據可讀
                recv_data = fobj.recv(1024)
                if len(recv_data) > 0:
                    print('from %s : %s' % (str(fobj.getpeername()), recv_data.decode('gbk')))
                else:  # 客戶端關閉socket
                    print('%s close' % str(fobj.getpeername()))
                    fobj.close()  # 將關閉socket從rlist列表中刪除,表示不再關注這個socket
                    rlist.remove(fobj)


if __name__ == "__main__":
    main()

Num12-->epoll版--TCP服務器實現

Test01-->epoll的優(yōu)點:

沒有最大并發(fā)連接的限制,能打開的FD(指的是文件描述符,通俗的理解就是套接字對應的數字編號)的上限遠大于1024
效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;即epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環(huán)境中,epoll的效率就會遠遠高于select和poll。

Test02-->一些術語

EPOLLIN (可讀)
EPOLLOUT (可寫)
EPOLLET (ET模式)
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。

LT模式是默認模式,LT模式與ET模式的區(qū)別如下:

LT模式:當epoll檢測到描述符事件發(fā)生并將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll時,會再次響應應用程序并通知此事件。

ET模式:當epoll檢測到描述符事件發(fā)生并將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll時,不會再次響應應用程序并通知此事件。

Test03-->案例的實現代碼

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke

import socket
import select

# 創(chuàng)建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 設置可以重復使用綁定的信息
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 綁定本機信息
s.bind(("", 8080))

# 變?yōu)楸粍?s.listen(10)

# 創(chuàng)建一個epoll對象
epoll = select.epoll()

# 測試,用來打印套接字對應的文件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET

# 注冊事件到epoll中
# epoll.register(fd[, eventmask])
# 注意,如果fd已經注冊過,則會發(fā)生異常
# 將創(chuàng)建的套接字添加到epoll的事件監(jiān)聽中
epoll.register(s.fileno(), select.EPOLLIN | select.EPOLLET)

connections = {}
addresses = {}

# 循環(huán)等待客戶端的到來或者對方發(fā)送數據
while True:

    # epoll 進行 fd 掃描的地方 -- 未指定超時時間則為阻塞等待
    epoll_list = epoll.poll()

    # 對事件進行判斷
    for fd, events in epoll_list:

        # print fd
        # print events

        # 如果是socket創(chuàng)建的套接字被激活
        if fd == s.fileno():
            conn, addr = s.accept()

            print('有新的客戶端到來%s' % str(addr))

            # 將 conn 和 addr 信息分別保存起來
            connections[conn.fileno()] = conn
            addresses[conn.fileno()] = addr

            # 向 epoll 中注冊 連接 socket 的 可讀 事件
            epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)


        elif events == select.EPOLLIN:
            # 從激活 fd 上接收
            recvData = connections[fd].recv(1024)

            if len(recvData) > 0:
                print('recv:%s' % recvData)
            else:
                # 從 epoll 中移除該 連接 fd
                epoll.unregister(fd)

                # server 側主動關閉該 連接 fd
                connections[fd].close()

                print("%s---offline---" % str(addresses[fd]))

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容