1. 背景介紹
1.1 TCP與IP協(xié)議
python的socket
模塊是網(wǎng)絡(luò)編程的基礎(chǔ)組件主要用于主機(jī)或者進(jìn)程之間的通信。
計(jì)算機(jī)網(wǎng)絡(luò)的TCP/IP五層模型中,傳輸層的TCP協(xié)議和UDP協(xié)議實(shí)現(xiàn)了主機(jī)間的通信。其中TCP協(xié)議需要先建立連接,然后進(jìn)行數(shù)據(jù)傳輸,如果出現(xiàn)丟包情況會(huì)進(jìn)行數(shù)據(jù)重傳,確保數(shù)據(jù)送達(dá)目的地。而UDP協(xié)議是無(wú)連接的,不需要先建立連接,只需要知道主機(jī)地址就可以直接將數(shù)據(jù)傳過(guò)去,并不保證數(shù)據(jù)一定送到目的地,但是因此傳輸速度比較快。
因此在常規(guī)的模型中,如果進(jìn)行大量數(shù)據(jù)的即時(shí)傳輸(比如視頻電話等)通常是先使用TCP建立連接,然后使用UDP進(jìn)行數(shù)據(jù)傳輸。(事實(shí)上隨著技術(shù)進(jìn)步,有些視頻類(lèi)通信是根據(jù)網(wǎng)絡(luò)狀況選擇通信協(xié)議,在網(wǎng)絡(luò)狀況良好時(shí)會(huì)使用完全TCP的通信。)
1.2 客戶(hù)端與服務(wù)器端
在網(wǎng)絡(luò)編程時(shí),主機(jī)間通信時(shí)通常是C/S架構(gòu),即一方做客戶(hù)端,一方做服務(wù)器端。一般來(lái)說(shuō)服務(wù)器端需要能夠同時(shí)處理多個(gè)客戶(hù)端的請(qǐng)求,因此實(shí)現(xiàn)的時(shí)候需要涉及到多線程的知識(shí)。
在TCP連接中,認(rèn)為主動(dòng)發(fā)起通信請(qǐng)求的一方是客戶(hù)端,被動(dòng)響應(yīng)請(qǐng)求的一方是服務(wù)器端。
1.3 環(huán)境
- 操作系統(tǒng):CentOS
- 編程語(yǔ)言:python 2.7.5
- python模塊:標(biāo)準(zhǔn)庫(kù)中的socket,time,threading。
2. TCP連接
2.1 客戶(hù)端
首先在頭部需要導(dǎo)入socket庫(kù)。
然后創(chuàng)建TCP連接的套接字(socket),并且指定服務(wù)器的主機(jī)地址和端口號(hào),發(fā)起連接請(qǐng)求。這里指定的是新浪的服務(wù)器,端口號(hào)為80。
import socket #導(dǎo)入socket庫(kù)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #創(chuàng)建套接字
s.connect(("www.sina.com.cn", 80)) #向新浪服務(wù)器的80端口發(fā)起連接請(qǐng)求
其中創(chuàng)建套接字的時(shí)候AF_INET
指定使用IPv4,如果想要使用IPv6可以改為AF_INET6
;SOCK_STREAM
指定面向流傳輸?shù)腡CP協(xié)議。
需要注意的是,發(fā)起連接請(qǐng)求的時(shí)候傳入的(hostname, port)
的一個(gè)元組,所以需要兩重括號(hào)。這里的hostname
可以是像上面的網(wǎng)址,DNS協(xié)議會(huì)自動(dòng)把網(wǎng)址解析成對(duì)應(yīng)的主機(jī)IP;也可以是IP地址,比如本地IP"127.0.0.1"
。如果是自己平時(shí)做實(shí)驗(yàn)的話,port
端口號(hào)需要大于1024,否則可能會(huì)和其他服務(wù)的端口號(hào)沖突。
服務(wù)器如果響應(yīng)連接請(qǐng)求,就會(huì)和客戶(hù)端建立連接。之后就可以向服務(wù)器發(fā)起請(qǐng)求進(jìn)行通信??梢允褂胹end函數(shù)發(fā)送請(qǐng)求。比如在廖雪峰 Python網(wǎng)絡(luò)編程的教程中發(fā)送的請(qǐng)求為:
s.send('GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')
這個(gè)請(qǐng)求是按照http協(xié)議的格式發(fā)出,得到的響應(yīng)是帶有http首部的新浪首頁(yè)html內(nèi)容。之后就可以接受這些內(nèi)容并查看。
buffer = []
while True:
d = s.recv(1024)#recv()函數(shù)中的參數(shù)表示一次最多接受的字節(jié)數(shù),這里表示一次最多接受1kb
if d:
buffer.append(d)
else:
break
data = ''.join(buffer)
header, html = data.split('\r\n\r\n',1)#分離http首部和html內(nèi)容
print 'header:\n', header#打印首部
最后結(jié)束通信,調(diào)用close函數(shù)關(guān)閉連接。
s.close()
2.2 服務(wù)器端
服務(wù)器端與客戶(hù)端類(lèi)似,創(chuàng)建一個(gè)基于IPv4和TCP協(xié)議的套接字(socket)之后,需要先綁定服務(wù)器的IP地址和端口號(hào)。這是因?yàn)橐慌_(tái)機(jī)器可能有多塊網(wǎng)卡,具有不同的IP地址。然后使用listen()函數(shù)進(jìn)行監(jiān)聽(tīng)該端口是否有客戶(hù)端發(fā)送請(qǐng)求過(guò)來(lái)。如果接收到請(qǐng)求,則創(chuàng)建一個(gè)新的線程處理這個(gè)連接請(qǐng)求。
def tcp_server(host,port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host, port))#綁定IP地址和端口號(hào)
s.listen(5)#監(jiān)聽(tīng)端口,指定最大連接數(shù)
print 'Waiting for connections......'
while True:
sock, addr = s.accept()#接受一個(gè)新連接
t = threading.Thread(target = tcplink, args = (sock, addr))#創(chuàng)建新線程處理TCP連
接
t.start()
其中target = tcplink
指定的是新線程中調(diào)用的函數(shù),args = (sock, addr)
指定的是tcplink()函數(shù)的參數(shù)。
def tcplink(sock, addr):
print 'Accept new connection from %s :%s......' % addr
sock.send('Welcome!')
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
與這個(gè)服務(wù)器端程序相對(duì)應(yīng)的客戶(hù)端程序見(jiàn)完整代碼。
3. UDP連接
3.1 客戶(hù)端
創(chuàng)建套接字的時(shí)候,SOCK_DGRAM
指定是UDP連接。
客戶(hù)端不再需要connect()
發(fā)起連接,而是通過(guò)sendto()
直接指定服務(wù)器的(hostname, port)
元組。但是依然可以用recv()
接收服務(wù)器端發(fā)送的數(shù)據(jù)。
def udp_connect(host, port, msg):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in msg:
s.sendto(data,(host,port))
print s.recv(1024)
s.close()
3.2 服務(wù)器端
服務(wù)器端也不再需要使用listen()
進(jìn)行監(jiān)聽(tīng),只需要通過(guò)recvfrom()
獲取客戶(hù)端發(fā)送的數(shù)據(jù)和(hostname, port)
元組。
def udp_server(host,port):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind((host,port))
print 'Bind UDP on %s:%s......'%(host, port)
while True:
data, addr = s.recvfrom(1024)
print 'Received from %s :%s......' % addr
s.sendto('Hello,%s~'%data, addr)
4. 遇到的問(wèn)題
在編程實(shí)現(xiàn)的過(guò)程中遇到不少問(wèn)題,排除拼寫(xiě)錯(cuò)誤之外,值得一記的一個(gè)錯(cuò)誤是端口被占用。python報(bào)錯(cuò)為:
Couldn't listen on any:9999: [Errno 98] Address already in use.
這是因?yàn)榉?wù)器端程序終止運(yùn)行之后該進(jìn)程仍在占用那個(gè)端口進(jìn)行監(jiān)聽(tīng)。這時(shí)候可以先查找占用端口的進(jìn)程PID,使用kill命令強(qiáng)行終止進(jìn)程。
$ lsof -i TCP:9999 | grep LISTEN
$ lsof -i UDP:9999
$ kill -s 9 <PID>
5. 其他
參考資料
- 廖雪峰 Python網(wǎng)絡(luò)編程
- 菜鳥(niǎo)教程 Python網(wǎng)絡(luò)編程
- 計(jì)算機(jī)網(wǎng)絡(luò)基礎(chǔ)知識(shí)總結(jié)
完整代碼
Github/zc12345