socket 和 網(wǎng)絡(luò)I/O模型

《UNIX 網(wǎng)絡(luò)編程卷一:套接字聯(lián)網(wǎng)API》筆記

套接字

套接字編程接口,是在 TCP/IP 協(xié)議族中,應(yīng)用層進(jìn)入傳輸層的接口。用套接字編寫使用 TCP或UDP 的網(wǎng)絡(luò)應(yīng)用程序。應(yīng)用層是用戶進(jìn)程,下面是系統(tǒng)內(nèi)核的一部分功能。

原始套接字,raw socket,應(yīng)用不使用傳輸層協(xié)議,直接用IP協(xié)議,例如:OSPF

協(xié)議族與I/O

套接字(socket): 一個標(biāo)識端點(diǎn)的 IP、端口 組合。{ip : port}

套接字對(socket pair): 通過兩端端點(diǎn)的四元組,唯一地標(biāo)識一個連接。{本地ip : 本地port,對端ip : 對端port}

監(jiān)聽套接字(listening socket),{* : *},等待連接請求,被動打開。服務(wù)器的監(jiān)聽套接字對用{x.x.x.x : port_num,* : *}表示,匹配所有源/對端ip和源/對端端口

已連接套接字(connected socket),accept 返回的,三次握手完畢的套接字,已連接套接字對用{x.x.x.x : port_num,ip : port}表示,已連接套接字使用與監(jiān)聽套接字相同的本地IP和本地端口(單宿主服務(wù)器)。

TCP的幾個狀態(tài)

TCP狀態(tài)轉(zhuǎn)換圖

套接字的幾個函數(shù)對應(yīng)的TCP操作

以單進(jìn)程阻塞I/O,也就是 迭代服務(wù)器(同一時間只能處理一個客戶請求) 為例

套接字函數(shù)
TCP連接的分組交換

socket

創(chuàng)建套接字,指定要使用的地址簇(ip)和套接字類型/傳輸層協(xié)議(tcp),返回一個套接字描述符,簡稱sockfd,用于在函數(shù)調(diào)用(connect、read 等)中標(biāo)識這個套接字。

bind

將一個 本地IP:本地端口 賦值給一個套接字,限定該套接字只接收目的地為指定的本地IP、端口的客戶連接。進(jìn)程指定。

linsen

僅由 TCP server 調(diào)用。當(dāng) socket 函數(shù)創(chuàng)建了套接字,會假設(shè)為主動套接字,也就是將調(diào)用 connect 主動發(fā)起連接的套接字。linsen 將一個未連接的套接字轉(zhuǎn)換成被動套接字,通知內(nèi)核應(yīng)該接受指向該套接字的連接請求(被動打開)。此時套接字的狀態(tài)從 CLOSED 轉(zhuǎn)到 LISTEN。

內(nèi)核為每個監(jiān)聽套接字維護(hù)兩個隊(duì)列:

  • 未完成連接隊(duì)列,收到了客戶的 SYN,正在等待 TCP 三次握手完成。此時套接字的狀態(tài)為 SYN_RCVD。
  • 已完成連接隊(duì)列,已經(jīng)完成 TCP 三次握手的客戶,這些套接字處于 ESTABLISHED 狀態(tài)。

當(dāng)收到客戶的SYN,TCP在未完成連接隊(duì)列中創(chuàng)建一項(xiàng)新條目,同時繼承監(jiān)聽套接字的參數(shù),然后返回SYS、ACK,這一項(xiàng)一直保留在未完成連接隊(duì)列中,直到收到 ACK,或者該項(xiàng)超時。如果三次握手正常完成,該項(xiàng)從未完成連接隊(duì)列移到已完成連接隊(duì)列的隊(duì)尾。

指定 backlog,即兩個隊(duì)列長度之和。

TCP為監(jiān)聽套接字維護(hù)的兩個隊(duì)列

accept

僅由 TCP server 調(diào)用,從已完成連接的隊(duì)頭獲得一個已完成連接,如果已完成連接的隊(duì)列為空,進(jìn)程被掛起進(jìn)入睡眠狀態(tài)(假定套接字為默認(rèn)的阻塞方式),直到隊(duì)列中有條目。

接收監(jiān)聽套接字描述符,返回 由內(nèi)核自動為獲得的已完成連接生成的已連接套接字描述符,以及對應(yīng)的已連接套接字

connect

僅由 TCP client 調(diào)用,接收 socket 返回的套接字描述符,主動向 TCP server 發(fā)送建立連接的請求(主動打開),觸發(fā)三次握手。調(diào)用 connect 之前不一定要調(diào)用 bind 綁定本地ip 和 本地端口,這樣,內(nèi)核會通過數(shù)據(jù)出口確定源ip,并選定一個臨時端口作為源端口。

fork、exec

調(diào)用一次,返回兩次

  • 在父進(jìn)程中返回一次,返回的是新的子進(jìn)程ID號(pid),用于記錄并跟蹤所有子進(jìn)程
  • 在子進(jìn)程中返回一次,始終為0,因?yàn)槊總€子進(jìn)程都只有一個父進(jìn)程,而且可以通過getppid獲取父ID(ppid)

父進(jìn)程調(diào)用fork前打開的所有描述符,在fork返回后,與子進(jìn)程共享(復(fù)制程序代碼與描述符)

每個 文件/套接字 都有一個引用計(jì)數(shù)器,在文件表項(xiàng)中維護(hù),表示當(dāng)前打開的 引用了該文件/套接字的描述符 個數(shù)。close 只是將計(jì)數(shù)值減一,真正清理套接字、釋放資源,需要計(jì)數(shù)值為0。

fork 的兩種典型用法:

  • 一個進(jìn)程想要執(zhí)行另一個程序

fork,新創(chuàng)一個進(jìn)程,在內(nèi)存中運(yùn)行相同的程序代碼
exec,換成另一個程序代碼

fork、exec

比如,shell 執(zhí)行可執(zhí)行程序文件

  • 一個進(jìn)程創(chuàng)建自身的副本,各自同時處理各自的操作

網(wǎng)絡(luò)服務(wù)器

fork 后判斷

fork 的限制在于,操作系統(tǒng)對于運(yùn)行服務(wù)器的用戶ID,能夠同時擁有的子進(jìn)程數(shù)的限制。

close

當(dāng)套接字描述符的引用為0,發(fā)送 FIN 關(guān)閉套接字,終止 TCP 連接。

默認(rèn)行為是將該套接字標(biāo)記為已關(guān)閉,然后立即返回。這個套接字的描述符不能再被 read 或 write 使用。

shutdown

不管引用計(jì)數(shù)器,直接發(fā)送 FIN,可以半關(guān)閉連接。

并發(fā)服務(wù)器

前面提到的都是 迭代服務(wù)器,一次只能處理一個客戶請求,如果希望同時服務(wù)多個客戶,可以通過為每個客戶 fork 一個子進(jìn)程進(jìn)行服務(wù)來實(shí)現(xiàn)。

當(dāng)一個連接建立時,accept 返回 已連接套接字描述符(connfd),服務(wù)器接著調(diào)用 fork 創(chuàng)建子進(jìn)程,監(jiān)聽套接字描述符和新的已連接套接字描述符在父進(jìn)程和新的子進(jìn)程之間共享/被復(fù)制,由子進(jìn)程通過 connfd 處理客戶請求,然后父進(jìn)程關(guān)閉已連接套接字描述符,子進(jìn)程關(guān)閉監(jiān)聽套接字描述符。最終,子進(jìn)程只處理一個已連接套接字描述符,父進(jìn)程在監(jiān)聽套接字上調(diào)用 accept 接受下一個客戶連接。

任何進(jìn)程在任何時候可以擁有的打開的描述符數(shù)量是有限的。

exit

EOF 字符,當(dāng)使用control + C終止連接時發(fā)送。通過調(diào)用 exit 正常終止連接。

  • 客戶端調(diào)用

由內(nèi)核關(guān)閉所有打開的描述符,發(fā)送 FIN 給服務(wù)器(TCP 狀態(tài) FIN-WAIT-1),并在收到服務(wù)器的 FIN 后,回復(fù) ACK (TCP 狀態(tài) FIN-WAIT-2)

  • 服務(wù)器子進(jìn)程調(diào)用

由子進(jìn)程關(guān)閉 已連接套接字描述符,發(fā)送 FIN 給客戶端,收到 ACK

在服務(wù)器子進(jìn)程終止時,會給父進(jìn)程發(fā)送 SIGCHLD 信號,默認(rèn)的處理行為是忽略,父進(jìn)程不做處理,這樣會導(dǎo)致子進(jìn)程進(jìn)入僵死狀態(tài)(z)

信號(signal)

通知某個進(jìn)程發(fā)生了某個事件,也稱為軟中斷(software interrupt),信號通常是異步發(fā)生的,即進(jìn)程預(yù)先不知道信號什么時候會發(fā)生

信號可以:

  • 由一個進(jìn)程發(fā)送給另一個進(jìn)程(或自身)
  • 由內(nèi)核發(fā)給某個進(jìn)程

SIGCHLD 是內(nèi)核在任一進(jìn)程終止時,發(fā)給父進(jìn)程的信號

每個信號都有一個關(guān)聯(lián)的處置/行為,可以設(shè)置信號的處理函數(shù),捕獲信號并處理

處理 SIGCHLD 信號

設(shè)置僵死(zombie)狀態(tài)的目的,是維護(hù)子進(jìn)程的信息,以便父進(jìn)程在以后某個時候獲取。如果一個進(jìn)程終止,而該進(jìn)程有子進(jìn)程處于僵死狀態(tài),父進(jìn)程ID重置為1(init進(jìn)程),繼承這些子進(jìn)程的init進(jìn)程將清理它們(wait 去除僵死狀態(tài))

留存僵死進(jìn)程會占用內(nèi)核空間,可能導(dǎo)致進(jìn)程資源耗盡。

建立一個信號處理函數(shù)并在其中調(diào)用 wait 不足以防止出現(xiàn)僵死進(jìn)程,因?yàn)椋?/p>

幾個連接同時終止,會同時發(fā)送多個 FIN,服務(wù)器幾個子進(jìn)程同時終止,同時有幾個 SIGCHLD 信號傳給父進(jìn)程,信號不會排隊(duì)等待依次處理,信號處理函數(shù)只執(zhí)行一次,其他的變?yōu)榻┧肋M(jìn)程,這時需要循環(huán)調(diào)用 waitpid,指定進(jìn)程。

需要在一個循環(huán)中調(diào)用 waitpid,以獲取所有已終止子進(jìn)程的狀態(tài)。

# -*- coding: utf-8 -*-
import socket

#創(chuàng)建socket對象
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#連接服務(wù)器,指定地址和端口號
s.connect(('127.0.0.1',10000))

#打印收到的消息
print s.recv(1024)

for data in ['xu','zhang','ting']:
    #發(fā)送數(shù)據(jù)
    s.send(data)
    #打印收到的數(shù)據(jù)
    print s.recv(1024)

#發(fā)送消息以關(guān)閉連接
s.send('exit')

s.close()

I/O 復(fù)用

在單進(jìn)程中,當(dāng)同時處理多個 I/O 時(比如 套接字 I/O 和 終端 I/O),進(jìn)程會處理一個 I/O,阻塞于另一個 I/O,這需要進(jìn)程預(yù)先通知內(nèi)核有哪些 I/O,讓內(nèi)核一旦發(fā)現(xiàn)其中有 I/O 就緒,就通知進(jìn)程。這就是I/O 復(fù)用,可以由 select 或 poll 函數(shù)實(shí)現(xiàn)。

調(diào)用 select,允許進(jìn)程通知內(nèi)核,等待多個事件中的任一個發(fā)生,并只在有一個或多個事件發(fā)生,或經(jīng)歷一段指定時間后再喚醒進(jìn)程。

三種情況發(fā)生才會返回:

  • 一個集合中的任何描述符準(zhǔn)備好被讀取
  • 一個集合中的任何描述符準(zhǔn)備好被寫入
  • 一個集合中的任何描述符有異常需要處理

即,用 select 告訴內(nèi)核,對那些描述符的 讀取、寫入、異常 感興趣,不限于套接字描述符

有同時等待的最大描述符數(shù)限制。同時,由于對所有描述符公平掃描,一些長時間靜止的描述符與常用描述符地位相同,浪費(fèi)了資源。

只會將已連接描述符放在描述符集中。

套接字選項(xiàng)

getsockopt、setsockopt 函數(shù)

可以獲取、設(shè)置影響套接字的選項(xiàng),需要:

  • 一個打開的套接字描述符

  • 設(shè)置 level,指明設(shè)置的是哪一個級別的選項(xiàng),通用的套接字選項(xiàng)、特定協(xié)議的可選項(xiàng)(IP、IPv6、IPv4 or IPv6、ICMPv6、TCP)

  • optname,選項(xiàng)名

獲取、設(shè)置選項(xiàng)的時機(jī)
部分TCP已連接套接字從監(jiān)聽套接字繼承選項(xiàng)

SO_REUSEADDR,允許重用本地地址

fcntl 函數(shù)

(file control,文件控制),對描述符進(jìn)行控制操作,比如,設(shè)置描述符為 非阻塞I/O 或 信號驅(qū)動。

用 F_SETFL 設(shè)置 O_NONBLOCK 文件狀態(tài)標(biāo)識位,非阻塞式 I/O
用 F_SETFL 設(shè)置 O_ASYNC 文件狀態(tài)標(biāo)識位,信號驅(qū)動式 I/O

UNIX 下的五種 I/O 模型

  • 阻塞式 I/O
  • 非阻塞式 I/O
  • I/O 復(fù)用(select 和 poll)
  • 信號驅(qū)動式 I/O
  • 異步 I/O(POSIX)

一個輸入操作的兩個階段

  1. 等待數(shù)據(jù)準(zhǔn)備好。對于套接字,是等待從網(wǎng)絡(luò)收到數(shù)據(jù),并且在數(shù)據(jù)到達(dá)后,復(fù)制數(shù)據(jù)到內(nèi)核緩沖區(qū)
  2. 從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù)到進(jìn)程緩沖區(qū),以便進(jìn)程處理

阻塞式 I/O

默認(rèn)情況下,所有套接字都是阻塞的。

發(fā)起一個不能立即完成的套接字調(diào)用,進(jìn)程阻塞,等待調(diào)用結(jié)果。

可能阻塞的套接字調(diào)用/函數(shù):

  1. 輸入操作。當(dāng)進(jìn)程對一個會阻塞的套接字調(diào)用 read、recvfrom 等函數(shù)時,如果接受緩沖區(qū)沒有數(shù)據(jù)可讀,進(jìn)程會進(jìn)入睡眠狀態(tài),直到數(shù)據(jù)到達(dá)。
  2. 輸出操作。當(dāng)進(jìn)程調(diào)用 write、sendto 等函數(shù)時,內(nèi)核將從應(yīng)用程序的緩沖區(qū)復(fù)制數(shù)據(jù)到套接字的發(fā)送緩沖區(qū),如果發(fā)送緩沖區(qū)沒有空間,進(jìn)程會進(jìn)入睡眠,直到有空間。
  3. 接受連接。當(dāng)進(jìn)程調(diào)用 accept 函數(shù),但沒有新的已完成連接,進(jìn)程會進(jìn)入睡眠狀態(tài)。
  4. 發(fā)起連接。當(dāng)進(jìn)程調(diào)用 connect 函數(shù),發(fā)送 SYN,等待服務(wù)器的 ACK 時。
阻塞式I/O

非阻塞式 I/O

將套接字設(shè)置為非阻塞,是告訴內(nèi)核,當(dāng)進(jìn)程請求的 I/O 操作結(jié)果需要等待時,不阻塞進(jìn)程,而是立即返回一個錯誤,以便進(jìn)程可以做其他事情,但需要不時回來查看一下結(jié)果,即輪詢(polling),這樣做會耗費(fèi)大量CPU時間。

非阻塞式I/O

I/O 復(fù)用(select 和 poll)

調(diào)用 select 或 poll,進(jìn)程阻塞于這兩個調(diào)用,而不是真正的 I/O 上。

阻塞于 select 后,就等待套接字變?yōu)榭勺x,表示有數(shù)據(jù)已經(jīng)準(zhǔn)備好,可以讀取。當(dāng) select 返回套接字可讀的消息,會從內(nèi)核復(fù)制數(shù)據(jù)到進(jìn)程。

I/O 復(fù)用的優(yōu)點(diǎn)是,可以同時等待多個描述符就緒。

但有同時等待的最大描述符數(shù)限制。同時,由于對所有描述符公平掃描,一些長時間靜止的描述符與常用描述符地位相同,浪費(fèi)了資源。

I/O 復(fù)用、多線程 使用的是阻塞式 I/O。但前者由單進(jìn)程處理多個客戶,后者為每個客戶派生一個子進(jìn)程進(jìn)行處理。

I/O復(fù)用

chat server.py

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

import socket,select

def broadcast_data(sock,message):
    for socket in CONNECTION_LIST:
        if socket != server_socket and socket != sock:
            try:
                socket.send(message)
            except:
                socket.close()
                CONNECTION_LIST.remove(socket)

if __name__ == '__main__':
    CONNECTION_LIST = []
    RECE_BUFFER = 4096
    PORT = 5000
    server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    #print server_socket
    #<socket._socketobject object at 0x1004da050>
    
    server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)

    server_socket.bind(('0.0.0.0',PORT))
    server_socket.listen(10)
    CONNECTION_LIST.append(server_socket)
    print 'Chat server started on port ' + str(PORT)

    while 1:
        read_sockets,write_sockets,error_sockets = select.select(CONNECTION_LIST,[],[])
        # print read_sockets
        #[<socket._socketobject object at 0x1004da050>]
        
        for sock in read_sockets:
            if sock == server_socket:
                print sock
                #<socket._socketobject object at 0x1004da050>
                sockfd,addr = server_socket.accept()
                #print sockfd,addr
                #<socket._socketobject object at 0x1004e68a0> ('127.0.0.1', 61045)
                
                CONNECTION_LIST.append(sockfd)
                #print CONNECTION_LIST
                #[<socket._socketobject object at 0x1004da050>, <socket._socketobject object at 0x1004e68a0>]
                #[<socket._socketobject object at 0x1004da050>, <socket._socketobject object at 0x1004e68a0>, <socket._socketobject object at 0x1004fd130>]
                
                print 'Client (%s,%s) connected' % addr
                broadcast_data(sockfd,'[%s:%s] entered room\n' % addr)
            else:
                try:
                    data = sock.recv(RECE_BUFFER)
                    print data
                    #hi
                    if data:
                        broadcast_data(sock,'\r' + '<' + str(sock.getpeername()) + '>' + data)
                except:

                    addr = sock.getpeername()

                    broadcast_data(sock,'Client (%s,%s) is offline' % addr)
                    print 'Client (%s,%s) is offline' % addr
                    sock.close()
                    CONNECTION_LIST.remove(sock)
                    continue
    server_socket.close()

chat client.py

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

import socket,select,string,sys

def prompt():
    sys.stdout.write('<You> ')
    sys.stdout.flush()

if __name__ == '__main__':
    if len(sys.argv) < 3 :
        print 'Usage : python telnet.py hostname port'
        sys.exit()
    host = sys.argv[1]
    port = int(sys.argv[2])
    
    s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.settimeout(2)

    try:
        s.connect((host,port))
    except :
        print 'Unable to connect'
        sys.exit()
        
    print 'Connected to remote host. Start sending messages'
    prompt()
    while 1:
        rlist = [sys.stdin,s]
        read_list,write_list,error_list = select.select(rlist,[],[])
        for sock in read_list:
            if sock == s:
                data = sock.recv(4096)
                if not data :
                    print 'Disconnected from chat server'
                    sys.exit()
                else:
                    sys.stdout.write(data)
                    prompt()
            else:
                msg = sys.stdin.readline(
                s.send(msg)
                prompt()

信號驅(qū)動式 I/O

需要先開啟套接字的信號驅(qū)動式 I/O 功能,用 sigaction 安裝一個信號處理函數(shù)。由于調(diào)用后會立即返回,因此進(jìn)程不會被阻塞,而是繼續(xù)處理其他事情,一旦數(shù)據(jù)準(zhǔn)備好了,內(nèi)核會向進(jìn)程返回 SIGIO 信號,通知已經(jīng)準(zhǔn)備好被讀取,可以開始一個 I/O 操作,或者,可以讓進(jìn)程直接處理數(shù)據(jù)。

優(yōu)勢是,等待數(shù)據(jù)準(zhǔn)備好的階段不會阻塞。

信號驅(qū)動式I/O

異步 I/O

(由 POSIX 定義)

會通知內(nèi)核進(jìn)行某個 I/O 操作,并讓內(nèi)核在整個 I/O 操作完成后 回復(fù)消息 給進(jìn)程。不需要像 select 或 poll 主動詢問,也沒有了詢問描述符的數(shù)量限制

與信號驅(qū)動式的不同在于,前者通知 I/O 操作已經(jīng)完成,而后者通知可以啟動一個 I/O 操作。

要使用 異步I/O,需要給內(nèi)核傳遞描述符、完成后如何通知進(jìn)程 等。

調(diào)用后,會立即返回,在整個輸入操作的 等待 和 復(fù)制 期間,進(jìn)程都不會阻塞。

同步 I/O 與異步 I/O的區(qū)別是,真正的 I/O 操作(從內(nèi)核復(fù)制到進(jìn)程)階段,同步 I/O 也不會阻塞進(jìn)程。

異步I/O

五種I/O 模型比較

五種I/O模型比較

守護(hù)進(jìn)程

inetd,超級服務(wù)器

線程

通常,當(dāng)一個進(jìn)程需要另一個實(shí)體完成某事,就 fork 一個子進(jìn)程去處理,但:

  • fork 代價昂貴。需要將父進(jìn)程的內(nèi)存代碼、所有描述符等復(fù)制到子進(jìn)程,雖然有 寫時復(fù)制(copy-on-write) 確保只在需要時操作,但代價依然很大。
  • fork 后,父子進(jìn)程之間信息的傳遞需要 進(jìn)程間通信(IPC)機(jī)制。fork時,父進(jìn)程可以很容易向子進(jìn)程傳遞消息,因?yàn)樽舆M(jìn)程是父進(jìn)程的一個拷貝,但從子進(jìn)程返回信息給父進(jìn)程卻很費(fèi)力。

所以,出現(xiàn)了一個解決方法:多線程。

線程,是輕量級的進(jìn)程(lightweight process),創(chuàng)建速度比進(jìn)程快很多。每個進(jìn)程中至少有一個線程。

同一個進(jìn)程中的所有線程共享相同的全局內(nèi)存,使得線程之間易于共享信息,但因此會有同步問題(一個線程對信息的修改,需要同步給其他線程。類似協(xié)同文檔編輯)。共享的有:進(jìn)程執(zhí)行的代碼、大多數(shù)數(shù)據(jù)、描述符、信號處理函數(shù)(是某種線程)、當(dāng)前工作目錄、用戶ID、組ID 等。

create

當(dāng)一個程序由exec啟動執(zhí)行時,會創(chuàng)建一個進(jìn)程,同時在進(jìn)程中自動創(chuàng)建稱為初始線程/主線程的單個線程。其余線程通過create函數(shù)創(chuàng)建(create 作用類似 fork),每個線程由一個線程ID標(biāo)識。創(chuàng)建時,需要指定由線程執(zhí)行的函數(shù)、參數(shù),線程通過調(diào)用函數(shù)執(zhí)行,然后顯示(用 exit)或隱式(函數(shù)返回)終止。

在線程之間不存在父子關(guān)系,所有線程(除了創(chuàng)建進(jìn)程時自動被創(chuàng)建的初始線程)都在相同的層次級別上。線程并不維護(hù)已創(chuàng)建線程的列表,也不知道創(chuàng)建它的線程。

join

(類似 waitpid)允許線程等待一個指定線程的終止。阻塞正在調(diào)用的線程,直到指定的線程終止。

detach

一個線程可以分為兩種:

  • 可匯合的/非脫離的(joinable,默認(rèn))

當(dāng)線程終止時,線程ID 和 退出狀態(tài)將留存,直到另一個線程對它調(diào)用 join。如果一個線程需要知道另一個線程什么時候終止,需要保持第二個線程的可匯合狀態(tài)。

  • 脫離的(detached)

類似守護(hù)進(jìn)程,當(dāng)終止時,釋放所有相關(guān)資源。

detach 函數(shù)將指定線程轉(zhuǎn)為脫離狀態(tài)。

close

主線程不關(guān)閉已連接套接字,因?yàn)橥贿M(jìn)程中的所有線程共享全部描述符,一旦主線程關(guān)閉套接字,會終止相應(yīng)連接。而且,創(chuàng)建新線程不會影響已打開描述符的引用計(jì)數(shù)。

tcp server.py

# -*- coding: utf-8 -*-
import socket,threading,time

#創(chuàng)建一個ipv4的TCP協(xié)議socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#綁定要監(jiān)聽的網(wǎng)卡和端口號。
#可以是某一塊網(wǎng)卡的IP地址,可以是‘0.0.0.0’表示的所有網(wǎng)絡(luò)地址,也可以是‘127.0.0.1’表示的本地內(nèi)部地址
#‘127.0.0.1’的客戶端必須在同一臺設(shè)備運(yùn)行并建立連接。
s.bind(('127.0.0.1',10000))

#開始監(jiān)聽,用參數(shù)指定可以接入多少連接
s.listen(5)
print 'Waiting for connection...'

def tcplink(sock,addr):
    print 'Accept new connection from %s:%s..' % addr  #('127.0.0.1', 55608)
    sock.send('Welcome!') #sock的方法?
    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 #('127.0.0.1', 55608)

#通過一個永久循環(huán)接受來自客戶端的連接請求
while True:
    #接受一個新連接
    sock,addr = s.accept()
    #print sock,addr
    #<socket._socketobject object at 0x1005fee50> ('127.0.0.1', 55608)
    
    #創(chuàng)建新線程處理TCP連接
    t = threading.Thread(target=tcplink,args=(sock,addr))
    t.start()

tcp client.py

# -*- coding: utf-8 -*-
import socket

#創(chuàng)建socket對象
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#連接服務(wù)器,指定地址和端口號
s.connect(('127.0.0.1',10000))

#打印收到的消息
print s.recv(1024)

for data in ['xu','zhang','ting']:
    #發(fā)送數(shù)據(jù)
    s.send(data)
    #打印收到的數(shù)據(jù)
    print s.recv(1024)

#發(fā)送消息以關(guān)閉連接
s.send('exit')

s.close()

線程安全(thread-safe)

能被線程化的程序調(diào)用的函數(shù)必須是線程安全的,可以同時被兩個及以上線程調(diào)用,并且能正確處理在各個線程中的變量,不會互相有影響。保證函數(shù)線程安全的一個方法是使用線程特定數(shù)據(jù),對于使用靜態(tài)變量的函數(shù),被多個線程同時調(diào)用會有問題,靜態(tài)變量無法為不同線程保存各自的值。

同步

為了有效地相互作用,線程必須同步其活動。這包括:

  • 通過修改共享數(shù)據(jù)進(jìn)行隱式通信
  • 通過相互通知所發(fā)生的事件進(jìn)行顯式通信

線程庫提供了以下同步機(jī)制:

  • 互斥鎖
  • 條件變量
  • join

條件變量 和 互斥鎖

共享的同步問題,解決方法:

條件變量(信號機(jī)制) + 互斥鎖(互斥機(jī)制)

條件變量必須始終與互斥鎖一起使用。給定的條件變量只能與一個互斥鎖關(guān)聯(lián),但是互斥鎖可用于多個條件變量。

  • 互斥鎖

多個線程同時訪問某個共享變量/全局變量,會有同步問題,線程編程,也稱為 并發(fā)編程/并行編程,因?yàn)槎鄠€線程可以并發(fā)地(或平行地)運(yùn)行且訪問相同的變量。

多個線程更改同一個共享變量的問題,可以用互斥鎖對這個變量進(jìn)行保護(hù)。如果想訪問這個變量,需要先獲得這個變量的互斥鎖(上鎖),當(dāng)一個線程對變量使用完畢,會釋放這個互斥鎖。如果試圖上鎖一個已經(jīng)被其他某個線程鎖住的變量,該線程會被阻塞,直到該變量被解鎖。

  • 條件變量

當(dāng)某個線程視圖使用的一個共享變量被其他線程鎖住,通常,會進(jìn)入睡眠,在主循環(huán)輪詢檢查,直到某個線程通知它有事做,才會醒來。但這樣輪詢很浪費(fèi) CPU 資源。

作為主循環(huán)的替代,出現(xiàn)了條件變量,條件變量允許線程一直等待,直到一些事件或者條件發(fā)生。每個條件變量都要關(guān)聯(lián)一個互斥鎖,因?yàn)?code>條件通常是線程共享的某個變量的值。允許不同線程設(shè)置、測試該變量,這要求有一個與該變量關(guān)聯(lián)的互斥鎖。

九種 TCP 服務(wù)器的設(shè)計(jì)

迭代服務(wù)器

單進(jìn)程、單線程,一次只能處理一個請求

并發(fā)服務(wù)器

每個客戶一個進(jìn)程/線程

預(yù)先派生子進(jìn)程,accept 無上鎖保護(hù)

  1. 并非收到請求后才派生
  2. 多個進(jìn)程對同一個監(jiān)聽描述符調(diào)用 accept 返回已連接套接字,對請求進(jìn)行處理,最終關(guān)閉
  3. 所有子進(jìn)程調(diào)用 accept 后睡眠,當(dāng)一個客戶連接到達(dá),所有子進(jìn)程都被喚醒(驚群),但只有最先運(yùn)行的進(jìn)程獲得連接,其他進(jìn)程繼續(xù)睡眠
  4. 會有 select 沖突,除非進(jìn)程阻塞于 accept 而不是 select

預(yù)先派生子進(jìn)程,accept 使用文件鎖保護(hù)

  1. 任一時刻只有一個子進(jìn)程阻塞在 accept 調(diào)用中,其他阻塞于獲取 保護(hù) accept 的鎖
  2. 文件鎖,涉及文件系統(tǒng)操作,會比較耗時
  3. 有些系統(tǒng)不允許多個進(jìn)程對 引用同一個監(jiān)聽套接字的描述符 調(diào)用accept

預(yù)先派生子進(jìn)程,accept 使用線程鎖保護(hù)

對于上面的第三點(diǎn),可以改用線程鎖保護(hù) accept。互斥鎖變量定義在進(jìn)程共享的內(nèi)存區(qū),同時需要告知進(jìn)程 這個是共享的互斥鎖

預(yù)先派生子進(jìn)程,傳遞描述符

只由父線程調(diào)用 accept,將已連接套接字描述符 傳遞 給某個子進(jìn)程。不需要對 accept 使用線程鎖保護(hù)。
但需要先創(chuàng)建 字節(jié)流管道,再 fork ,父進(jìn)程關(guān)閉已連接套接字描述符,子進(jìn)程關(guān)閉監(jiān)聽套接字描述符,然后父進(jìn)程通過 select 跟蹤子進(jìn)程的狀態(tài),選擇空閑子進(jìn)程傳遞描述符。

并發(fā)服務(wù)器,每個客戶一個線程

預(yù)先創(chuàng)建線程,每個線程各自 accept

使用互斥鎖,同一時刻只有一個線程調(diào)用 accept

預(yù)先創(chuàng)建線程,主線程統(tǒng)一 accept

  1. 只由主線程調(diào)用 accept,并將客戶連接傳遞給某個空閑線程
  2. 互斥鎖 + 條件變量

同步 異步 與 阻塞 非阻塞

同步、異步,是指通信機(jī)制,通過什么方式回答結(jié)果

  • 同步,調(diào)用后,如果沒有結(jié)果,調(diào)用不返回任何消息
  • 異步,調(diào)用后,直接返回,但并不是返回結(jié)果,被調(diào)用者,通過狀態(tài)通知來通知調(diào)用者

阻塞、非阻塞,是指程序等待調(diào)用結(jié)果時的狀態(tài)

  • 阻塞,在調(diào)用結(jié)果返回前,當(dāng)前進(jìn)程/線程被阻塞,直到被調(diào)用者返回結(jié)果才繼續(xù)
  • 非阻塞,即使不能立即得到結(jié)果,也不會阻塞當(dāng)前進(jìn)程/線程,可以偶爾回來查看有無結(jié)果

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內(nèi)容