16.1 介紹
16.1.1 什麼是客戶/服務器架構?
硬件的客戶/服務器架構
軟件客戶/服務器架構
16.1.2 客戶/服務器網絡編程
16.2 套接字: 通訊端點
16.2.1 什麼是套接字?
套接字是一種具有通訊端點感念的計算機網絡數據結構
16.2.2 套接字地址:主機與端口
主機和端口類似區號和電話號碼的一對組合
合法的端口號範圍是0到65535,小於1024的端口號爲系統保留端口
16.2.3 面向連接與無連接
面向連接(TCP)
套接字只有兩種一種是面向連接套接字,即在通訊之前一定要建立一條連接,這種通訊方式提供了順序的,可靠的不會重複的數據傳輸,每一份要發送的信息都會拆分成多份,每份都會不多不少的到達目的地後重新按順序拼裝起來,傳給正在等待的應用程序
實現這種連接的主要協議就是傳輸控制協議(即TCP)
要創建TCP套接字就得在創建的時候指定套接字類型爲SOCK_STREAM,表示爲流套接字
無連接(UDP)
與虛電路相反的數據報型是無連接套接字,即無需建立連接就可以進行通訊,這意味着數據到達的順序,可靠性及數據不重複性就無法保證,數據會保留在數據邊界,數據不會像TCP協議那樣被拆封爲小塊
使用數據報來傳輸數據就像郵政服務,郵件包裹不一定會按照他們發送的順序到達,而且可能還到達不了,而且還可能被重傳
由於面向連接套接字提供一些維持虛電路連接的開銷,數據報較他來說基本上沒有負擔,所以它能更好的提供性能,適合於某些應用場合
實現這種連接的主要協議就是用戶數據報協議(即UDP)
要創建UDP套接字就得在創建的時候指定套接字類型爲SOCK_DGRAM,即datagram數據報
由於這些套接字使用Internet協議來查找網絡中的主機,這樣形成的整個系統一般都會由這兩對協議(TCP/IP)和(UDP/IP)來提及
16.3 Python中的網絡編程
本節我們主要使用socket模塊,模塊中的socket()函數被用來創建套接字,其有自己的一套函數來提供基於套接字的網絡傳輸
16.3.1 socket()模塊函數:
創建套接字語法:
socket(socket_family,socket_type,protocol=0)
socket_family可以是AF_UNIX或AF_INET,soket_type可以是SOCK_STREAM或SOCK_DGRAM,protocal一般不填,默認爲0
創建一個TCP/IP套接字,需要調用socket.socket()
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
創建一個UDP/IP套接字,需要調用socket.socket()
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
使用from socket import * 將socket模塊裏的所有屬性帶到命名空間裏
當我們創建了套接字對象後,所有的交互豆漿通過對該套接字對象的方法進行調用
16.3.2 套接字對象(內建)方法
函數描述
s.bind()綁定地址(主機,端口號對)到套接字
s.listen()開始TCP監聽
s.accept()被動接受TCP服務器連接,(阻塞式)等待連接到來
s.connect()主動初始化TCP服務器連接
s.connect_ex()connect()函數的擴展版本,出錯時返回出錯碼,而不是拋異常公共用途的套接字函數
s.recv()接收TCP數據
s.send()發送TCP數據
s.sendall()完整發送TCP數據
s.recvfrom()接收UDP數據
s.sendto()發送UDP數據
s.getpeername()連接到當前套接字的遠端地址
s.getsockname()當前套接字的地址
s.getsockopt()返回指定套接字的參數
s.setsockopt()設置指定套接字的參數
s.close()關閉套接字
s.setblocking()設置套接字的阻塞與非阻塞模式
s.settimeout()設置阻塞套接字操作的超時時間
s.gettimeout()得到阻塞套接字操作的超市時間
面向文件的套接字的函數
s.fileno()曹姐字的文件描述符
s.makefile()創建一個與該套接字關聯的文件
16.3.3 創建一個TCP服務器
ss.socket()#創建服務器套接字
ss.bind()#把地址綁定到套接字上
ss.listen()#監聽連接
inf_loop()#服務器無限循環
cs=ss.accept()#接受客戶的連接
comm_loop()#通訊循環
cs.recv()/cs.send()#對話(接收與發送)
cs.close()#關閉客戶套接字
ss.close()#關閉服務器套接字(可選)
所有套接字都用socket().socket()函數創建,服務器需要"坐在某個端口上"等待請求,所以需要綁定到一個本地地址上,TCP服務器負責監聽連接,設置完,服務器就可以進行無限循環了
默認服務器會調用accept()阻塞式函數等待連接,來之前程序一直會處於掛起狀態
一旦接收到一個連接,accept()函數就會返回一個單獨的客戶的套接字用於後續通訊.
例,tsTserv.py文件會創建一個TCP服務程序,這個程序會把客戶發過來的字符串加上一個時間戳(格式:'[時間]數據')返回給客戶
# vi tsTserv.py
--------------------------------
#!/usr/bin/env python
#coding: UTF-8
from socket import *
from time import ctime
HOST = '' #綁定IP
PORT = 21567 #端口號
BUFSIZ = 1024 #緩衝1K
ADDR = (HOST,PORT)
tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR) #套接字
tcpSerSock.listen(5) #最大連接數
while True:
print 'waiting for connection...'
tcpCliSock,addr = tcpSerSock.accept()
print '...connected from: ', addr
while True:
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
tcpCliSock.send('[%s] %s'%(ctime(),data))
print [ctime()],':',data
tcpCliSock.close()
tcpSerSock.close()
--------------------------------
16.3.4 創建TCP客戶端
cs = socket()#創建客戶套接字
cs.connect()#嘗試連接服務器
comm_loop#通訊循環
cs.send()/cs.recv()#對話(發送/接收)
cs.close()#關閉客戶套接字
所有套接字都由socket.socket()函數創建,在客戶有了套接字之後,可以調用connect()函數去連接服務器,連接服務器後,就可以與服務器對話,對話結束可關閉套接字結束連接
例,程序連接到服務器,提示用戶輸入要傳輸的數據,然後顯示服務器返回的加了時間戳的結果
# vi tsTclnt.py
-------------------------------
#!/usr/bin/env python
from socket import *
HOST = '192.168.8.18'
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
while True:
data = raw_input('>')
if not data:
break
tcpCliSock.send(data)
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
print data
tcpCliSock.close()
-------------------------------
16.3.5 運行我們的客戶端與服務器程序
# python tsTserv.py
-----------------------------
waiting for connection...
-----------------------------
# python tsTclnt.py
----------------------------------
>send test data
[Thu Dec 19 12:46:36 2013] send test data
>hi
[Thu Dec 19 12:46:51 2013] hi
>how are you?
[Thu Dec 19 12:47:08 2013] how are you?
-----------------------------------
# python tsTserv.py
--------------------------------------
waiting for connection...
...connected from: ('192.168.8.19', 56868)
['Thu Dec 19 12:46:36 2013'] : send test data
['Thu Dec 19 12:46:51 2013'] : hi
['Thu Dec 19 12:47:08 2013'] : how are you?
---------------------------------------
核心提示:
"友好地"退出的一個方法就是把服務器無限循環放在一個try-except語句中try子句中,並捕獲EOFError和KeyboardInterrupt異常,在異常處理子句中,調用close()函數關閉服務器的套接字
例:
# vi tsTserv.py
--------------------------------
root@ubuntu:~/python# vi tsTserv.py
#!/usr/bin/env python
#coding: UTF-8
from socket import *
from time import ctime
HOST = '' #綁定IP
PORT = 21567 #端口號
BUFSIZ = 1024 #緩衝1K
ADDR = (HOST,PORT)
tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR) #套接字
tcpSerSock.listen(5) #最大連接數
try:
while True:
print 'waiting for connection...'
tcpCliSock,addr = tcpSerSock.accept()
print '...connected from: ', addr
while True:
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
tcpCliSock.send('[%s] %s'%(ctime(),data))
print [ctime()],':',data
except EOFError,KeyboardInterrupt:
tcpCliSock.close()
tcpSerSock.close()
--------------------------------
# vi tsTclnt.py
-------------------------------
#!/usr/bin/env python
from socket import *
HOST = '192.168.8.18'
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
try:
while True:
data = raw_input('>')
if not data:
break
tcpCliSock.send(data)
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
print data
except EOFError,KeyboardInterrupt:
tcpCliSock.close()
-------------------------------
16.3.6 創建一個UDP服務器
ss = socket()#創建一個服務器套接字
ss.bind()#綁定服務器套接字
inf_loop:#服務器無限循環
cs = ss.recvfrom()/ss.sendto()#對話(接收與發送)
ss.close()#關閉服務器套接字
例,創建一個能接收客戶的消息,在消息前加一個時間戳後返回的UDP服務器
# vi tsUserv.py
-----------------------------
#!/usr/bin/env python
from socket import *
from time import ctime
HOST = ''
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST,PORT)
udpSerSock = socket(AF_INET, SOCK_DGRAM)
udpSerSock.bind(ADDR)
while True:
print 'waiting for message...'
data, addr = udpSerSock.recvfrom(BUFSIZ)
udpSerSock.sendto('[%s] [%s]' %(ctime(), data),addr)
print '...received from and returned to:', addr
udpSerSock.close()
-----------------------------
UDP和TCP服務器的另一個重要區別是,由於數據報套接字是無連接的,所以無法把客戶的鏈接將誒另外的套接字進行後續通訊,這些服務器只是接受消息,需要的話,給客戶返回一個結果就可以了
16.3.7 創建一個UDP客戶端
cs = socket()#創建客戶套接字
comm_loop:#通訊循環
cs.sendto()/cs.recvfrom()#對話(發送/接收)
cs.close()#關閉客戶套接字
在套接字對象創建好之後,我們就進入一個與服務器的對話循環,在通訊結束後,套接字就被關閉了
例,創建一個UDP客戶端,程序會提示用戶輸入要傳給服務器的信息,顯示服務器返回的加了時間戳的結果
# vi tsUclnt.py
-------------------------------------
#!/usr/bin/env python
from socket import *
from time import ctime
HOST = ''
PORT = 21568
BUFSIZ = 1024
ADDR = (HOST,PORT)
udpSerSock = socket(AF_INET, SOCK_DGRAM)
udpSerSock.bind(ADDR)
while True:
print 'waiting for message...'
data, addr = udpSerSock.recvfrom(BUFSIZ)
udpSerSock.sendto('[%s] %s' %(ctime(), data),addr)
print '...received from and returned to:', addr
print data
udpSerSock.close()
-------------------------------------
# vi tsUclnt.py
-----------------------------------------
#!/usr/bin/env python
from socket import *
HOST = '192.168.8.18'
PORT = 21568
BUFSIZ = 1024
ADDR = (HOST, PORT)
udpCliSock = socket(AF_INET,SOCK_DGRAM)
while True:
data = raw_input('> ')
if not data:
break
udpCliSock.sendto(data, ADDR)
data, ADDR = udpCliSock.recvfrom(BUFSIZ)
if not data:
break
print data
udpCliSock.close()
-----------------------------------------
16.3.8 執行UDP服務器和客戶端
# python tsUserv.py
---------------------------------
waiting for message...
----------------------------------
# python tsUclnt.py
---------------------------------
> hi
[Thu Dec 19 17:25:01 2013] hi
> test1
[Thu Dec 19 17:25:03 2013] test1
> test2
[Thu Dec 19 17:25:05 2013] test2
----------------------------------
# python tsUserv.py
----------------------------------
waiting for message...
...received from and returned to: ('192.168.8.19', 46359)
hi
waiting for message...
...received from and returned to: ('192.168.8.19', 46359)
test1
waiting for message...
...received from and returned to: ('192.168.8.19', 46359)
test2
waiting for message...
-----------------------------------
16.3.9 套接字模塊屬性
屬性名字描述
AF_UNIX,AF_INET,AF_INET6Python支持的套接字家族
SO_STREAM,SO_DGRAM套接字類型(TCP=流,UDP=數據報)
has_ipv6標識是否支持IPV6的標誌變量
異常
error套接字相關錯誤
herror主機和地址相關的錯誤
gaierror地址相關錯誤
timeout超時
函數
socket()用指定的地址家族,套接字類型和協議類型(可選)創建一個套接字對象
socketpair()用指定的地址家族,套接字類型和協議類型(可選)創建一個套接字對象
fromfd()用一個已經打開的額文件描述符創建一個套接字對象
數據屬性
ssl()在套接字初始化一個安全套接字層(SSL),不做整數驗證
getaddrinfo()得到地址信息
getfqdn()返回完整的域的名字
gethostname()得到當前主機名
gethostbyname()由主機名得到對應的ip地址
gethostbyname()_ex()gethostbyname()擴展版本,返回主機名,主機所有的別名和IP地址列表
gethostbyaddr()由IP地址得到DNS信息,返回一個類似gethostbyname()_ex()的三元組
getprotobyname()由協議名(如"tcp")得到對應的號碼
getservbyname()/由服務名得到對應的端口號或相反
getservbyport()兩個函數中,協議名都是可選的
ntohl()/htohs()把一個整數由網絡字節序轉爲主機字節序
htonl()/htons()把一個整數由主機字節序轉爲網絡字節序
inet_aton()/把IP地址轉爲32位整形,以及反向函數(僅對IPV4有效)
inet_ptoa()
inet_pton()/把IP地址轉爲二進制格式以及反響函數(僅對IPV4有效)
inet_ntop()
getdefaulttimeout()/得到/設置默認的套接字超時時間,單位秒(浮點數)
setdefaulttimeout()
16.4 SocketServer模塊
SocketServer是標準庫中一個高級別的模塊,用於簡化網絡客戶與服務器的實現,模塊中,已經實現了一些可供使用的類
SocketServer模塊的類
類描述
BaseServer包含服務器的核心功能與混合(mix-in)類的鉤子功能,這個類用於派生,不要直接生成
這個類的類對象,可以考慮使用TCPServer和IDPServer
TCPServer/基本的網絡同步TCP/UDP服務器
UDPServer
UnixStreamServer/基本的基於文件同步TCP/UDP服務器
UnixDatagramServer
ForkingMixIn/實現了核心的進程化或線程化的功能,用於與服務器類進行混合(mix-in),以提供一些異步特性.
ThreadingMixIn不要直接生成這個類的對象
ForkingTCPServer/ForkingMixIn和TCPServer/UDPServer的組合
ForkingUDPServer
ThreadingTCPServer/ThreadingMixIn和TCPServer/UDPServer組合
ThreadingUDPServer
BaseRequestHandler包含處理服務請求的核心功能,只用於派生新的類,不要直接生成這個類的對象
可以考慮使用StreamRequestHandler或DatagramRequestHandler
StreamRequestHandler/TCP/UDP服務器的請求處理類的一個實現
DatagramRequestHandler
16.4.1 創建一個SocketServerTCP服務器
使用SocketServer裏的TCPServer和StreamRequestHandler類創建一個時間戳TCP服務器
# vi tsTservSS.py
---------------------------------------------
#!/usr/bin/env python
from SocketServer import (TCPServer as TCP,StreamRequestHandler as SRH)
from time import ctime
HOST = ''
PORT = 21567
ADDR = (HOST, PORT)
class MyRequestHandler(SRH):
def handle(self):
print "...connected from: ", self.client_address
self.wfile.write('[%s] %s'% (ctime(), self.rfile.readline()))
tcpServ = TCP(ADDR, MyRequestHandler)
print 'waiting for connection...'
tcpServ.serve_forever()
---------------------------------------------
16.4.2 創建SocketServerTCP客戶端
例,這是一個時間戳TCP客戶端,它知道如何與SocketServer裏StreamRequestHandler對象進行
# vi tsTclntSS.py
------------------------------------
#!/usr/bin/env python
from socket import *
HOST = 'localhost'
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
while True:
tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
data = raw_input('> ')
if not data:
break
tcpCliSock.send('%s\r\n' % data)
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
print data.strip()
tcpCliSock.close()
------------------------------------
16.4.2 執行TCP服務器和客戶端
# python tsTservSS.py
---------------------------------------
waiting for connection...
...connected from: ('127.0.0.1', 40712)
...connected from: ('127.0.0.1', 40713)
...connected from: ('127.0.0.1', 40714)
...connected from: ('127.0.0.1', 40715)
...connected from: ('127.0.0.1', 40716)
...connected from: ('127.0.0.1', 40717)
----------------------------------------
# python tsTclntSS.py
---------------------------------------
> test1
[Fri Dec 20 04:44:02 2013] test1
> test2
[Fri Dec 20 04:44:16 2013] test2
> test3
[Fri Dec 20 04:44:18 2013] test3
----------------------------------------
注:連接服務器連接了2次
16.5 Twisted框架介紹
Twisted是一個完全事件驅動的網絡框架,它允許你使用和開發完全異步的網絡應用程序和協議
16.5.1 創建一個Twisted Reactor TCP服務器
例,這是一個使用Twisted Internet類的時間戳TCP服務器
# vi tsTservTW.py
----------------------------------
#!/usr/bin/env python
from twisted.internet import protocol, reactor
from time import ctime
PORT = 21567
class TSServProtocol(protocol.Protocol):
def connectionMade(self):
clnt = self.clnt = self.transport.getPeer().host
print '...connected from: ',clnt
def dataReceived(self,data):
self.transport.write('[%s] %s' %(ctime(), data))
factory = protocol.Factory()
factory.protocol = TSServProtocol
print 'waiting for connection...'
reactor.listenTCP(PORT, factory)
reactor.run()
----------------------------------
16.5.2 創建一個Twisted Reactor TCP客戶端
# vi tsTclntTW.py
--------------------------------
#!/usr/bin/env python
from twisted.internet import protocol, reactor
HOST = 'localhost'
PORT = 21567
class TSClntProtocol(protocol.Protocol):
def sendData(self):
data = raw_input("> ")
if data:
print '...sending %s...' % data
self.transport.write(data)
else:
self.transport.loseConnection()
def connectionMade(self):
self.sendData()
def dataReceived(self, data):
print data
self.sendData()
class TSClntFactory(protocol.ClientFactory):
protocol = TSClntProtocol
clientConnectionLost = clientConnectionFailed = \
lambda self, connector, reason: reactor.stop()
reactor.connectTCP(HOST, PORT, TSClntFactory())
reactor.run()
--------------------------------
16.5.3 執行TCP服務器和客戶端
Twisted客戶顯示的內容與我們之前的客戶類似:
# python tsTservTW.py
---------------------------
waiting for connection...
----------------------------
# python tsTclntTW.py
-----------------------------------
> test1
...sending test1...
[Fri Dec 20 10:36:10 2013] test1
> test2
...sending test2...
[Fri Dec 20 10:36:15 2013] test2
------------------------------------
# python tsTservTW.py
----------------------------------
waiting for connection...
...connected from: 127.0.0.1
-----------------------------------
注: "connection from" 輸出沒有其他的信息,因爲我們只詢問服務器的transport對象的getPeer()函數要了主機地址的信息
16.6 相關模塊
網絡/套接字編程相關模塊
模塊描述
socket底層網絡接口,本章討論過
anycore/爲能異步處理客戶請求的網絡應用程序提供底層功能
select在單線程網絡服務器程序中,管理多個套接字連接
SocketServer包含了些網絡應用程序服務器所需要的高級別模塊,提供了完整的進程和線程版本