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é)議是無連接的,不需要先建立連接挣饥,只需要知道主機(jī)地址就可以直接將數(shù)據(jù)傳過去除师,并不保證數(shù)據(jù)一定送到目的地,但是因此傳輸速度比較快扔枫。
因此在常規(guī)的模型中汛聚,如果進(jìn)行大量數(shù)據(jù)的即時(shí)傳輸(比如視頻電話等)通常是先使用TCP建立連接,然后使用UDP進(jìn)行數(shù)據(jù)傳輸短荐。(事實(shí)上隨著技術(shù)進(jìn)步倚舀,有些視頻類通信是根據(jù)網(wǎng)絡(luò)狀況選擇通信協(xié)議,在網(wǎng)絡(luò)狀況良好時(shí)會(huì)使用完全TCP的通信忍宋。)
1.2 客戶端與服務(wù)器端
在網(wǎng)絡(luò)編程時(shí)痕貌,主機(jī)間通信時(shí)通常是C/S架構(gòu),即一方做客戶端糠排,一方做服務(wù)器端舵稠。一般來說服務(wù)器端需要能夠同時(shí)處理多個(gè)客戶端的請求,因此實(shí)現(xiàn)的時(shí)候需要涉及到多線程的知識(shí)。
在TCP連接中哺徊,認(rèn)為主動(dòng)發(fā)起通信請求的一方是客戶端室琢,被動(dòng)響應(yīng)請求的一方是服務(wù)器端。
1.3 環(huán)境
- 操作系統(tǒng):CentOS
- 編程語言:python 2.7.5
- python模塊:標(biāo)準(zhǔn)庫中的socket落追,time盈滴,threading。
2. TCP連接
2.1 客戶端
首先在頭部需要導(dǎo)入socket庫轿钠。
然后創(chuàng)建TCP連接的套接字(socket)巢钓,并且指定服務(wù)器的主機(jī)地址和端口號(hào),發(fā)起連接請求疗垛。這里指定的是新浪的服務(wù)器症汹,端口號(hào)為80。
import socket #導(dǎo)入socket庫
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #創(chuàng)建套接字
s.connect(("www.sina.com.cn", 80)) #向新浪服務(wù)器的80端口發(fā)起連接請求
其中創(chuàng)建套接字的時(shí)候AF_INET
指定使用IPv4贷腕,如果想要使用IPv6可以改為AF_INET6
烈菌;SOCK_STREAM
指定面向流傳輸?shù)腡CP協(xié)議。
需要注意的是花履,發(fā)起連接請求的時(shí)候傳入的(hostname, port)
的一個(gè)元組,所以需要兩重括號(hào)挚赊。這里的hostname
可以是像上面的網(wǎng)址诡壁,DNS協(xié)議會(huì)自動(dòng)把網(wǎng)址解析成對應(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)連接請求,就會(huì)和客戶端建立連接嚎朽。之后就可以向服務(wù)器發(fā)起請求進(jìn)行通信铺纽。可以使用send函數(shù)發(fā)送請求哟忍。比如在廖雪峰 Python網(wǎng)絡(luò)編程的教程中發(fā)送的請求為:
s.send('GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')
這個(gè)請求是按照http協(xié)議的格式發(fā)出狡门,得到的響應(yīng)是帶有http首部的新浪首頁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ù)器端與客戶端類似叛复,創(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)聽該端口是否有客戶端發(fā)送請求過來。如果接收到請求抖僵,則創(chuàng)建一個(gè)新的線程處理這個(gè)連接請求鲤看。
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)聽端口,指定最大連接數(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ù)器端程序相對應(yīng)的客戶端程序見完整代碼。
3. UDP連接
3.1 客戶端
創(chuàng)建套接字的時(shí)候蹈垢,SOCK_DGRAM
指定是UDP連接慷吊。
客戶端不再需要connect()
發(fā)起連接,而是通過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)聽,只需要通過recvfrom()
獲取客戶端發(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. 遇到的問題
在編程實(shí)現(xiàn)的過程中遇到不少問題堰酿,排除拼寫錯(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)聽触创。這時(shí)候可以先查找占用端口的進(jìn)程PID,使用kill命令強(qiáng)行終止進(jìn)程为牍。
$ lsof -i TCP:9999 | grep LISTEN
$ lsof -i UDP:9999
$ kill -s 9 <PID>
5. 其他
參考資料
完整代碼
Github/zc12345