《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
套接字(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)
套接字的幾個函數(shù)對應(yīng)的TCP操作
以單進(jìn)程阻塞I/O,也就是 迭代服務(wù)器(同一時間只能處理一個客戶請求) 為例
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ì)列長度之和。
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,換成另一個程序代碼
比如,shell 執(zhí)行可執(zhí)行程序文件
- 一個進(jìn)程創(chuàng)建自身的副本,各自同時處理各自的操作
網(wǎng)絡(luò)服務(wù)器
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)
一個輸入操作的兩個階段
- 等待數(shù)據(jù)準(zhǔn)備好。對于套接字,是等待從網(wǎng)絡(luò)收到數(shù)據(jù),并且在數(shù)據(jù)到達(dá)后,復(fù)制數(shù)據(jù)到內(nèi)核緩沖區(qū)
- 從內(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ù):
- 輸入操作。當(dāng)進(jìn)程對一個會阻塞的套接字調(diào)用 read、recvfrom 等函數(shù)時,如果接受緩沖區(qū)沒有數(shù)據(jù)可讀,進(jìn)程會進(jìn)入睡眠狀態(tài),直到數(shù)據(jù)到達(dá)。
- 輸出操作。當(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)入睡眠,直到有空間。
- 接受連接。當(dāng)進(jìn)程調(diào)用 accept 函數(shù),但沒有新的已完成連接,進(jìn)程會進(jìn)入睡眠狀態(tài)。
- 發(fā)起連接。當(dāng)進(jìn)程調(diào)用 connect 函數(shù),發(fā)送 SYN,等待服務(wù)器的 ACK 時。
非阻塞式 I/O
將套接字設(shè)置為非阻塞,是告訴內(nèi)核,當(dāng)進(jìn)程請求的 I/O 操作結(jié)果需要等待時,不阻塞進(jìn)程,而是立即返回一個錯誤,以便進(jìn)程可以做其他事情,但需要不時回來查看一下結(jié)果,即輪詢(polling),這樣做會耗費(fèi)大量CPU時間。
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)行處理。
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)備好的階段不會阻塞。
異步 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 模型比較
守護(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ù)
- 并非收到請求后才派生
- 多個進(jìn)程對同一個監(jiān)聽描述符調(diào)用 accept 返回已連接套接字,對請求進(jìn)行處理,最終關(guān)閉
- 所有子進(jìn)程調(diào)用 accept 后睡眠,當(dāng)一個客戶連接到達(dá),所有子進(jìn)程都被喚醒(驚群),但只有最先運(yùn)行的進(jìn)程獲得連接,其他進(jìn)程繼續(xù)睡眠
- 會有 select 沖突,除非進(jìn)程阻塞于 accept 而不是 select
預(yù)先派生子進(jìn)程,accept 使用文件鎖保護(hù)
- 任一時刻只有一個子進(jìn)程阻塞在 accept 調(diào)用中,其他阻塞于獲取 保護(hù) accept 的鎖
- 文件鎖,涉及文件系統(tǒng)操作,會比較耗時
- 有些系統(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
- 只由主線程調(diào)用 accept,并將客戶連接傳遞給某個空閑線程
- 互斥鎖 + 條件變量
同步 異步 與 阻塞 非阻塞
同步、異步,是指通信機(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é)果