第十七章 Python網絡編程

Socket簡介

在網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個鏈接的一端稱爲一個Socket(套接字),用於描述IP地址和端口。

建立網絡通信連接至少要一對端口號(Socket),Socket本質是編程接口(API),對TCP/IP的封裝,提供了網絡通信能力。

每種服務都打開一個Socket,並綁定到端口上,不同的端口對應不同的服務,就像http對應80端口。

Socket是面向C/S(客戶端/服務器)模型設計,客戶端在本地隨機申請一個唯一的Socket號,服務器擁有公開的socket,任何客戶端都可以向它發送連接請求和信息請求。

比如:用手機打電話給10086客服,你的手機號就是客戶端,10086客服是服務端。必須在知道對方電話號碼前提下才能與對方通訊。

Socket數據處理流程如圖:

wKiom1hFB3ihlqXzAAD9zEGLSeA858.png

17.1 socket

在Python中提供此服務的模塊是socket和SocketServer,下面是socket常用的類、方法:

方法描述
socket.socket([family[, type[, proto]]])socket初始化函數,(地址族,socket類型,協議編號)協議編號默認0
socket.AF_INETIPV4協議通信
socket.AF_INET6IPV6協議通信
socket.SOCK_STREAMsocket類型,TCP
socket.SOCK_DGRAMsocket類型,UDP
socket.SOCK_RAW原始socket,可以處理普通socker無法處理的報文,比如ICMP
socket.SOCK_RDM更可靠的UDP類型,保證對方收到數據
socket.SOCK_SEQPACKET可靠的連續數據包服務

socket.socket()對象有以下方法:

accept()接受連接並返回(socket object, address info),address是客戶端地址
bind(address)綁定socket到本地地址,address是一個雙元素元組(host,port)
listen(backlog)開始接收連接,backlog是最大連接數,默認1
connect(address)連接socket到遠程地址
connect_ex(address)連接socket到遠程地址,成功返回0,錯誤返回error值
getpeername()返回遠程端地址(hostaddr, port)
gettimeout()返回當前超時的值,單位秒,如果沒有設置返回none
recv(buffersize[, flags])接收來自socket的數據,buffersize是接收數據量
send(data[, flags])發送數據到socket,返回值是發送的字節數
sendall(data[, flags])發送所有數據到socket,成功返回none,失敗拋出異常
setblocking(flag)設置socket爲阻塞(flag是true)或非阻塞(flag是flase)

溫習下TCP與UDP區別:

TCP和UDP是OSI七層模型中傳輸層提供的協議,提供可靠端到端的傳輸服務。

TCP(Transmission Control Protocol,傳輸控制協議),面向連接協議,雙方先建立可靠的連接,再發送數據。適用於可靠性要求高的應用場景。

UDP(User Data Protocol,用戶數據報協議),面向非連接協議,不與對方建立連接,直接將數據包發送給對方,因此相對TCP傳輸速度快 。適用於可靠性要求低的應用場景。

17.1.1 TCP編程

下面創建一個服務端TCP協議的Socket演示下。

先寫一個服務端:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
HOST = ''                 # 爲空代表所有可用的網卡
PORT = 50007              # 任意非特權端口
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)   # 最大連接數
conn, addr = s.accept()   # 返回客戶端地址
print 'Connected by', addr
while 1:
    data = conn.recv(1024)   # 每次最大接收客戶端發來數據1024字節
    if not data: break       # 當沒有數據就退出死循環 
    print "Received: ", data # 打印接收的數據
    conn.sendall(data)       # 把接收的數據再發給客戶端
conn.close()

再寫一個客戶端:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
HOST = '192.168.1.120'    # 遠程主機IP
PORT = 50007              # 遠程主機端口
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall('Hello, world') # 發送數據
data = s.recv(1024)       # 接收服務端發來的數據
s.close()
print 'Received: ', data

寫好後,打開一個終端窗口執行:

# python socket-server.py
監聽中...
# 直到客戶端運行會接收到下面數據並退出
Connected by ('192.168.1.120', 37548)
Received:  Hello, world

再打開一個終端窗口執行:

# 如果端口監聽說明服務端運行正常

# netstat -antp |grep 50007
tcp        0      0 0.0.0.0:50007           0.0.0.0:*               LISTEN      72878/python
# python socket-client.py
Received: Hello, world

通過實驗瞭解搭到Socket服務端工作有以下幾個步驟:

1)打開socket

2)綁定到一個地址和端口

3)監聽進來的連接

4)接受連接

5)處理數據

17.1.2 UDP編程

服務端:

import socket
HOST = ''               
PORT = 50007             
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind((HOST, PORT))
while 1:
    data, addr = s.recvfrom(1024)
    print 'Connected by', addr
    print "Received: ", data
    s.sendto("Hello %s"% repr(addr), addr)
conn.close()

客戶端:

import socket
HOST = '192.168.1.99'                 
PORT = 50007             
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(data, (HOST, PORT))
data = s.recv(1024)
s.close()
print 'Received: ', data

運行方式與TCP編程一樣。

使用UDP協議時,服務端就少了listen()和accept(),不需要建立連接就直接接收客戶端的數據,也是把數據直接發送給客戶端。

客戶端少了connect(),同樣直接通過sendto()給服務器發數據。

而TCP協議則前提先建立三次握手。

17.1.3 舉一個更直觀的socket通信例子

客戶端發送bash命令,服務端接收到並執行,把返回結果迴應給客戶端。

服務端:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import subprocess
import socket
HOST = ''               
PORT = 50007             
try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind((HOST, PORT))
    s.listen(1)
except socket.error as e:
    s.close()
    print e
    sys.exit(1)
while 1:
    conn, addr = s.accept()
    print 'Connected by', addr
    while 1:
        # 每次讀取1024字節
        data = conn.recv(1024)
        if not data: # 客戶端關閉服務端會收到一個空數據
            print repr(addr) + " close."
            conn.close()
            break     
        print "Received: ", data
        cmd = subprocess.Popen(data, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        result_tuple = cmd.communicate()
        if cmd.returncode != 0 or cmd.returncode == None:
            result = result_tuple[1]
            # result = cmd.stderr.read()
        else:
            result = result_tuple[0]
            # result = cmd.stdout.read()  # 讀不到標準輸出,不知道爲啥,所以不用
        if result:
            conn.sendall(result)
        else:
            conn.sendall("return null")
s.close()

客戶端:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import socket
HOST = '192.168.1.120'   
PORT = 50007             
try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
except socket.error as e:
    s.close()
    print e
    sys.exit(1)
while 1:
    cmd = raw_input("Please input command: ")
    if not cmd: continue
    s.sendall(cmd)
    recv_data = s.recv(1024)
    print 'Received: ', recv_data
s.close()

查看運行效果,先運行服務端,再運行客戶端:

# python socket-server.py
Connected by ('192.168.1.120', 45620)
Received:  ls
Received:  touch a.txt
Received:  ls

# python socket-client.py
Please input command: ls
Received: 
socket-client.py
socket-server.py
Please input command: touch a.txt
Received:  return null
Please input command: ls
Received: 
a.txt
socket-client.py
socket-server.py
Please input command:

我想通過上面這個例子你已經大致掌握了socket的通信過程。

再舉一個例子,通過socket獲取本機網卡IP:

>>> socket.gethostname()
'ubuntu'
>>> socket.gethostbyname(socket.gethostname())
'127.0.1.1'
>>> s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
>>> s.connect(('10.255.255.255', 0))
>>> s.getsockname()
('192.168.1.120', 35765)
>>> s.getsockname()[0]
'192.168.1.120'


博客地址:http://lizhenliang.blog.51cto.com

QQ羣:323779636(Shell/Python運維開發羣


17.2 SocketServer

ScoketServer是Socket服務端庫,比socket庫更高級,實現了多線程和多線程,併發處理多個客戶端請求。

下面是幾個常用的類:

SocketServer.TCPServer(server_address

RequestHandlerClassbind_and_activate=True)

服務器類,TCP協議

SocketServer.UDPServer(server_address

RequestHandlerClassbind_and_activate=True)

服務器類,UDP協議

SocketServer.BaseServer(server_address

RequestHandlerClass)

這個是所有服務器對象的超類。它定義了接口,不提供大多數方法,在子類中進行。
SocketServer.BaseRequestHandler這個是所有請求處理對象的超類。它定義了接口,一個具體的請求處理程序子類必須定義一個新的handle()方法。
SocketServer.StreamRequestHandler流式socket,根據socket生成讀寫socket用的兩個文件對象,調用rfile和wfile讀寫
SocketServer.DatagramRequestHandler數據報socket,同樣生成rfile和wfile,但UDP不直接關聯socket。這裏rfile是由UDP中讀取的數據生成,wfile則是新建一個StringIO,用於寫數據
SocketServer.ForkingMixIn/ThreadingMixIn多進程(分叉)/多線程實現異步。混合類,這個類不會直接實例化。用於實現處理多連接

SocketServer.BaseServer()對象有以下方法:

fileno()返回一個整數文件描述符上服務器監聽的套接字
handle_request()處理一個請求
serve_forever(poll_interval=0.5)處理,直至有明確要求shutdown()的請求。輪訓關機每poll_interval秒
shutdown()告訴serve_forever()循環停止並等待
server_close()清理服務器
address_family地址族
server_address監聽的地址
RequestHandlerClass用戶提供的請求處理類
socketsocket對象上的服務器將監聽傳入的請求
allow_reuse_address服務器是否允許地址的重用。默認False
request_queue_size請求隊列的大小。
socket_typesocket類型。socket.SOCK_STREAM或socket.SOCK_DGRAM
timeout超時時間,以秒爲單位
finish_request()實際處理通過實例請求RequestHandleClass並調用其handle()方法
get_request()必須接受從socket的請求,並返回
handle_error(request, client_address)如果這個函數被條用handle()
process_request(request, client_address)?
server_activate()?
server_bind()由服務器構造函數調用的套接字綁定到所需的地址
verify_request(request, client_address)返回一個布爾值,如果該值是True,則該請求將被處理,如果是False,該請求將被拒絕。

創建一個服務器需要幾個步驟:

1)創建類,繼承請求處理類(BaseRequestHandler),並重載其handle()方法,此方法將處理傳入的請求

2)實例化服務器類之一,它傳遞服務器的地址和請求處理程序類

3)調用handle_request()或serve_forever()服務器對象的方法來處理一個或多個請求

4)調用server_close()關閉套接字

17.2.1 TCP編程

服務端:

#!/usr/bin/python
# -*- coding: utf-8 -*
import SocketServer
class MyTCPHandler(SocketServer.BaseRequestHandler):
    """
    請求處理程序類。
    每個連接到服務器都要實例化一次,而且必須覆蓋handle()方法來實現與客戶端通信
    """
    def handle(self):
        # self.request 接收客戶端數據
        self.data = self.request.recv(1024).strip()
        print "%s wrote:" % (self.client_address[0])
        print self.data
        # 把接收的數據轉爲大寫發給客戶端
        self.request.sendall(self.data.upper())
if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    # 創建服務器並綁定本地地址和端口
    server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler)
    # 激活服務器,會一直運行,直到Ctrl-C中斷
    server.serve_forever()

另一個請求處理程序類,利用流(類文件對象簡化通信提供標準文件接口):

class MyTCPHandler(SocketServer.StreamRequestHandler):
    def handle(self):
        # self.rfile創建的是一個類文件對象處理程序,就可以調用readline()而不是recv()
        self.data = self.rfile.readline().strip()
        print "%s wrote:" % (self.client_address[0])
        print self.data
        # 同樣,self.wfile是一個類文件對象,用於回覆客戶端
        self.wfile.write(self.data.upper())

客戶端:

import socket
import sys
HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    sock.connect((HOST, PORT))
    sock.sendall(data + "\n")
    received = sock.recv(1024)
finally:
    sock.close()
print "Sent: %s" % data
print "Received: %s" % received

服務端結果:

# python TCPServer.py
127.0.0.1 wrote:
hello
127.0.0.1 wrote:
nice

客戶端結果:

# python TCPClient.py hello
Sent: hello
Received: HELLO
# python TCPClient.py nice
Sent: nice
Received: NICE

17.2.2 UDP編程

服務端:

import SocketServer
class MyTCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        self.data = self.request[0].strip()
        self.socket = self.request[1]
        print "%s wrote:" % (self.client_address[0])
        print self.data
        self.socket.sendto(self.data.upper(), self.client_address)
if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    server = SocketServer.UDPServer((HOST, PORT), MyTCPHandler)
    server.serve_forever()

客戶端:

import socket
import sys
HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(data + "\n", (HOST, PORT))
received = sock.recv(1024)
print "Sent: %s" % data
print "Received: %s" % received

與TCP執行結果一樣。

17.2.3 異步混合

創建異步處理,使用ThreadingMixIn和ForkingMixIn類。

ThreadingMixIn類的一個例子:

#!/usr/bin/python
# -*- coding: utf-8 -*
import socket
import threading
import SocketServer
class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        data = self.request.recv(1024)
        cur_thread = threading.current_thread()
        response = "%s: %s" % (cur_thread.name, data)
        self.request.sendall(response)
class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass
def client(ip, port, message):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((ip, port))
    try:
        sock.sendall(message)
        response = sock.recv(1024)
        print "Received: %s" % response
    finally:
        sock.close()
if __name__ == "__main__":
    # 端口0意味着隨機使用一個未使用的端口
    HOST, PORT = "localhost", 0
    server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
    ip, port = server.server_address
    # 服務器啓動一個線程,該線程將開始。每個線程處理每個請求
    server_thread = threading.Thread(target=server.serve_forever)
    # 作爲守護線程
    server_thread.daemon = True
    server_thread.start()
    print "Server loop running in thread:", server_thread.name
    client(ip, port, "Hello World 1")
    client(ip, port, "Hello World 2")
    client(ip, port, "Hello World 3")
    server.shutdown()
      server.server_close()

# python socket-server.py
Server loop running in thread: Thread-1
Received: Thread-2: Hello World 1
Received: Thread-3: Hello World 2
Received: Thread-4: Hello World 3


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章