Socket是網(wǎng)絡編程的一個抽象概念庆锦。通常我們用一個Socket表示“打開了一個網(wǎng)絡鏈接”捅位,而打開一個Socket需要知道目標計算機的IP地址和端口號,再指定協(xié)議類型即可搂抒。
TCP編程
客戶端
# 要創(chuàng)建一個基于TCP連接的Socket艇搀,可以這樣做:
# 導入socket庫:
import socket
# 創(chuàng)建一個socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立連接:
s.connect(('www.sina.com.cn', 80))
# 創(chuàng)建Socket時,AF_INET指定使用IPv4協(xié)議求晶,如果要用更先進的IPv6焰雕,就指定為AF_INET6。SOCK_STREAM指定使用面向流的TCP協(xié)議誉帅,這樣淀散,一個Socket對象就創(chuàng)建成功,但是還沒有建立連接蚜锨。
# 客戶端要主動發(fā)起TCP連接档插,必須知道服務器的IP地址和端口號。新浪網(wǎng)站的IP地址可以用域名www.sina.com.cn自動轉換到IP地址亚再,但是怎么知道新浪服務器的端口號呢郭膛?
# 答案是作為服務器,提供什么樣的服務氛悬,端口號就必須固定下來则剃。由于我們想要訪問網(wǎng)頁,因此新浪提供網(wǎng)頁服務的服務器必須把端口號固定在80端口如捅,因為80端口是Web服務的標準端口棍现。其他服務都有對應的標準端口號,例如SMTP服務是25端口镜遣,F(xiàn)TP服務是21端口己肮,等等。端口號小于1024的是Internet標準服務的端口悲关,端口號大于1024的谎僻,可以任意使用。
# 注意參數(shù)是一個tuple寓辱,包含地址和端口號艘绍。
# 建立TCP連接后,我們就可以向新浪服務器發(fā)送請求秫筏,要求返回首頁的內(nèi)容:
# 發(fā)送數(shù)據(jù):
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')
# TCP連接創(chuàng)建的是雙向通道诱鞠,雙方都可以同時給對方發(fā)數(shù)據(jù)挎挖。但是誰先發(fā)誰后發(fā),怎么協(xié)調(diào)般甲,要根據(jù)具體的協(xié)議來決定肋乍。例如鹅颊,HTTP協(xié)議規(guī)定客戶端必須先發(fā)請求給服務器敷存,服務器收到后才發(fā)數(shù)據(jù)給客戶端。
# 發(fā)送的文本格式必須符合HTTP標準堪伍,如果格式?jīng)]問題锚烦,接下來就可以接收新浪服務器返回的數(shù)據(jù)了:
# 接收數(shù)據(jù):
buffer = []
while True:
# 每次最多接收1k字節(jié):
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
# 接收數(shù)據(jù)時,調(diào)用recv(max)方法帝雇,一次最多接收指定的字節(jié)數(shù)涮俄,因此,在一個while循環(huán)中反復接收尸闸,直到recv()返回空數(shù)據(jù)彻亲,表示接收完畢,退出循環(huán)吮廉。
# 當我們接收完數(shù)據(jù)后苞尝,調(diào)用close()方法關閉Socket,這樣宦芦,一次完整的網(wǎng)絡通信就結束了:
# 關閉連接:
s.close()
# 接收到的數(shù)據(jù)包括HTTP頭和網(wǎng)頁本身宙址,我們只需要把HTTP頭和網(wǎng)頁分離一下,把HTTP頭打印出來调卑,網(wǎng)頁內(nèi)容保存到文件:
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的數(shù)據(jù)寫入文件:
with open('sina.html', 'wb') as f:
f.write(html)
# 現(xiàn)在抡砂,只需要在瀏覽器中打開這個sina.html文件,就可以看到新浪的首頁了恬涧。
服務器
# 服務器進程首先要綁定一個端口并監(jiān)聽來自其他客戶端的連接注益。如果某個客戶端連接過來了,服務器就與該客戶端建立Socket連接溯捆,隨后的通信就靠這個Socket連接了丑搔。
# 所以,服務器會打開固定端口(比如80)監(jiān)聽现使,每來一個客戶端連接低匙,就創(chuàng)建該Socket連接。由于服務器會有大量來自客戶端的連接碳锈,所以顽冶,服務器要能夠區(qū)分一個Socket連接是和哪個客戶端綁定的。一個Socket依賴4項:服務器地址售碳、服務器端口强重、客戶端地址绞呈、客戶端端口來唯一確定一個Socket。
# 但是服務器還需要同時響應多個客戶端的請求间景,所以佃声,每個連接都需要一個新的進程或者新的線程來處理,否則倘要,服務器一次就只能服務一個客戶端了圾亏。
# 我們來編寫一個簡單的服務器程序,它接收客戶端連接封拧,把客戶端發(fā)過來的字符串加上Hello再發(fā)回去志鹃。
# 首先,創(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)絡地址陕见,還可以用127.0.0.1綁定到本機地址。127.0.0.1是一個特殊的IP地址味抖,表示本機地址评甜,如果綁定到這個地址,客戶端必須同時在本機運行才能連接非竿,也就是說蜕着,外部的計算機無法連接進來。
# 端口號需要預先指定红柱。因為我們寫的這個服務不是標準服務承匣,所以用9999這個端口號。請注意锤悄,小于1024的端口號必須要有管理員權限才能綁定:
# 監(jiān)聽端口:
s.bind(('127.0.0.1', 9999))
# 緊接著韧骗,調(diào)用listen()方法開始監(jiān)聽端口,傳入的參數(shù)指定等待連接的最大數(shù)量:
s.listen(5)
print('Waiting for connection...')
# 接下來零聚,服務器程序通過一個永久循環(huán)來接受來自客戶端的連接袍暴,accept()會等待并返回一個客戶端的連接:
while True:
# 接受一個新連接:
sock, addr = s.accept()
# 創(chuàng)建新線程來處理TCP連接:
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()
# 每個連接都必須創(chuàng)建新線程(或進程)來處理,否則隶症,單線程在處理連接的過程中政模,無法接受其他客戶端的連接:
def tcplink(sock, addr):
print('Accept new connection from %s:%s...' % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
time.sleep(1)
if not data or data.decode('utf-8') == 'exit':
break
sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' % addr)
# 連接建立后,服務器首先發(fā)一條歡迎消息蚂会,然后等待客戶端數(shù)據(jù)淋样,并加上Hello再發(fā)送給客戶端。如果客戶端發(fā)送了exit字符串胁住,就直接關閉連接趁猴。
# 要測試這個服務器程序刊咳,我們還需要編寫一個客戶端程序:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立連接:
s.connect(('127.0.0.1', 9999))
# 接收歡迎消息:
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
# 發(fā)送數(shù)據(jù):
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()
UDP編程
使用UDP協(xié)議時,不需要建立連接儡司,只需要知道對方的IP地址和端口號娱挨,就可以直接發(fā)數(shù)據(jù)包。
# 服務器首先需要綁定端口:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 綁定端口:
s.bind(('127.0.0.1', 9999))
# 創(chuàng)建Socket時捕犬,SOCK_DGRAM指定了這個Socket的類型是UDP跷坝。綁定端口和TCP一樣,但是不需要調(diào)用listen()方法或听,而是直接接收來自任何客戶端的數(shù)據(jù):
print('Bind UDP on 9999...')
while True:
# 接收數(shù)據(jù):
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)
# recvfrom()方法返回數(shù)據(jù)和客戶端的地址與端口探孝,這樣,服務器收到數(shù)據(jù)后誉裆,直接調(diào)用sendto()就可以把數(shù)據(jù)用UDP發(fā)給客戶端。
# 客戶端使用UDP時缸濒,首先仍然創(chuàng)建基于UDP的Socket足丢,然后,不需要調(diào)用connect()庇配,直接通過sendto()給服務器發(fā)數(shù)據(jù):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Michael', b'Tracy', b'Sarah']:
# 發(fā)送數(shù)據(jù):
s.sendto(data, ('127.0.0.1', 9999))
# 接收數(shù)據(jù):
print(s.recv(1024).decode('utf-8'))
s.close()
# 從服務器接收數(shù)據(jù)仍然調(diào)用recv()方法斩跌。