Socket網絡編程

1、什麼是Socket

  網絡上的兩個程序經過一個雙向的通訊鏈接實現數據的交換,這個鏈接的一端稱爲一個socket。
創建網絡通訊鏈接至少要一對端口號(socket)。socket本質是編程接口(API),對TCP/IP的封裝,TCP/IP也要提供可供程序員作網絡開發所用的接口,這就是Socket編程接口;HTTP是轎車,提供了封裝或者顯示數據的具體形式;Socket是發動機,提供了網絡通訊的能力。
  Socket的英文原義是「孔」或「插座」。做爲BSD UNIX的進程通訊機制,取後一種意思。一般也稱做"套接字",用於描述IP地址和端口,是一個通訊鏈的句柄,能夠用來實現不一樣虛擬機或不一樣計算機之間的通訊。在Internet上的主機通常運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,並綁定到一個端口上,不一樣的端口對應於不一樣的服務。Socket正如其英文原義那樣,像一個多孔插座。一臺主機猶如佈滿各類插座的房間,每一個插座有一個編號,有的插座提供220伏交流電, 有的提供110伏交流電,有的則提供有線電視節目。 客戶軟件將插頭插到不一樣編號的插座,就能夠獲得不一樣的服務。

2、Socket參數介紹

  在python中,實例化一個socket鏈接對象後,須要傳入的參數有如下幾項(括號中是默認參數):              
socket.socket(family=AF_INETtype=SOCK_STREAMproto=0fileno=None

Socket Families(地址簇)

socket.AF_UNIX unix本機進程間通訊 python

socket.AF_INET IPV4 程序員

socket.AF_INET6  IPV6編程

Socket Types

socket.SOCK_STREAM  #for tcp服務器

socket.SOCK_DGRAM   #for udp 網絡

socket.SOCK_RAW     #原始套接字,普通的套接字沒法處理ICMP、IGMP等網絡報文,而SOCK_RAW能夠;其次,SOCK_RAW也能夠處理特殊的IPv4報文;此外,利用原始套接字,能夠經過IP_HDRINCL套接字選項由用戶構造IP頭。併發

socket.SOCK_RDM  #是一種可靠的UDP形式,即保證交付數據報但不保證順序。SOCK_RAM用來提供對原始協議的低級訪問,在須要執行某些特殊操做時使用,如發送ICMP報文。SOCK_RAM一般僅限於高級用戶或管理員運行的程序使用。ssh

socket.SOCK_SEQPACKET #廢棄了socket

剩下的參數proto,fileno通常咱們不用管。tcp

3、socket實例

最簡單的socket實例ide

import socket

client = socket.socket()
client.connect(('localhost',9999))
client.send(b'hello world')
data = client.recv(1024)
print('recv',data)

client.close()
socket_client
import socket

server = socket.socket()
server.bind(('localhost',9999))
server.listen()
conn,addr = server.accept()
data = conn.recv(1024)
print('recv:',data)
conn.send(data.upper())

server.close()
socket_server

  上面的代碼的有一個問題, 就是SocketServer.py運行起來後, 接收了一次客戶端的data就退出了。。。, 但實際場景中,一個鏈接創建起來後,可能要進行屢次往返的通訊。

import socket

client = socket.socket()
client.connect(('localhost',8888))
while True:
    data = input('>>請輸入要發送的內容:')
    if len(data) == 0:
        continue
    client.send(data.encode('utf-8'))
    res = client.recv(1024)
    print(res.decode())

client.close()
socket_client 支持屢次交互
import socket

server = socket.socket()
server.bind(('localhost',8888))
server.listen(5)
print('我要開始等電話了')
conn,addr = server.accept()
print('電話來了')
while True:
    data = conn.recv(1024)
    print(data)
    if not data:
        print('client has lost')
        break
    conn.send(data.upper())

conn.close()
socket_server 支持屢次交互

  可是上面這種寫法仍是有缺陷的,雖然這種寫法能夠實現客戶端和服務器端屢次交互了,可是隻要客戶端斷開了,服務器端也會斷開,由於只有一個while循環,客戶端一斷開,服務器端收不到數據,就會直接break跳出循環,這樣程序就結束了。這還不是咱們想要的結果,咱們想要的是,客戶端斷開了,服務器端能夠接收下一個連進來的鏈接,而後和下一個連進來的客戶端再次的進行屢次的交互。

socket_client 支持屢次交互
socket_server 支持屢次交互
  那麼知道了socket的實現原理以後,咱們能夠經過socket實現一個極簡版的ssh工具,就是客戶端鏈接上服務器後,讓服務器執行命令,並返回結果給客戶端。
import socket

client = socket.socket()
client.connect(('localhost',8888))
while True:
    data = input('>>請輸入要發送的內容:')
    if len(data) == 0:
        continue
    client.send(data.encode('utf-8'))
    res_size = client.recv(1024)
    print(res_size)
    res_data = b''
    recv_size = 0
    while recv_size < int(res_size.decode()):
        data = client.recv(1024)
        recv_size += len(data)
        res_data += data
    else:
        print(recv_size)
        print(res_data.decode())

    print(res_data)

client.close()
socket_client_ssh
import socket
import os

server = socket.socket()
server.bind(('localhost',8888))
server.listen(5)
print('我要開始等電話了')
while True:
    conn,addr = server.accept()
    print('電話來了')
    while True:
        data = conn.recv(1024)
        if not data:
            print('client has lost')
            break
        res = os.popen(data.decode()).read()
        conn.send(str(len(res.encode())).encode())
        conn.send(res.encode())
        print('send done')

    conn.close()
socket_server_ssh

輸出結果:

  看程序執行報錯了, 我在客戶端本想只接服務器端命令的執行結果大小,但實際上卻連命令結果也跟着接收了一部分。

  這裏就引入了一個重要的概念,「粘包」, 即服務器端你調用時send 2次,但你send調用時,數據其實並無馬上被髮送給客戶端,而是放到了系統的socket發送緩衝區裏,等緩衝區滿了、或者數據等待超時了,數據纔會被send到客戶端,這樣就把好幾回的小數據拼成一個大數據,統一發送到客戶端了,這麼作的目地是爲了提升io利用效率,一次性發送總比連發好幾回效率高。 但也帶來一個問題,就是「粘包」,即2次或屢次的數據粘在了一塊兒統一發送了。就是咱們上面看到的狀況 。 

  咱們在這裏必需要想辦法把粘包分開, 由於不分開,你就沒辦法取出來服務器端返回的命令執行結果的大小。首先你是沒辦法讓緩衝區強制刷新把數據發給客戶端的。 你能作的,只有一個。就是讓緩衝區超時,超時了,系統就不會等緩衝區滿了,會直接把數據發走,那麼如何讓緩衝區超時呢?

答案就是:

  1. time.sleep(0.5),經屢次測試,讓服務器程序sleep 至少0.5就會形成緩衝區超時。雖然咱們以爲0.5s很少,可是對數據實時要求高的業務場景,好比股票交易等用這種方法確定是不行的。
  2.  不用sleep,服務器端每發送一個數據給客戶端,就馬上等待客戶端進行迴應,即調用 conn.recv(1024), 因爲recv在接收不到數據時是阻塞的,這樣就會形成,服務器端接收不到客戶端的響應,就不會執行後面的conn.sendall(命令結果)的指令,收到客戶端響應後,再發送命令結果時,緩衝區就已經被清空了,由於上一次的數據已經被強制發到客戶端了。看下面代碼實現。  
import socket

client = socket.socket()
client.connect(('localhost',8888))
while True:
    data = input('>>請輸入要發送的內容:')
    if len(data) == 0:
        continue
    client.send(data.encode('utf-8'))
    res_size = client.recv(1024)
    client.send(b'ok')
    print(res_size)
    res_data = b''
    recv_size = 0
    while recv_size < int(res_size.decode()):
        data = client.recv(1024)
        recv_size += len(data)
        res_data += data
    else:
        print(recv_size)
        print(res_data.decode())

client.close()
socket_client_ssh
import socket
import os

server = socket.socket()
server.bind(('localhost',8888))
server.listen(5)
print('我要開始等電話了')
while True:
    conn,addr = server.accept()
    print('電話來了')
    while True:
        data = conn.recv(1024)
        if not data:
            print('client has lost')
            break
        res = os.popen(data.decode()).read()
        conn.send(str(len(res.encode())).encode())
        conn.recv(1024)
        conn.sendall(res.encode())
        print('send done')

    conn.close()
socket_serevr_ssh

SocketServer

socketserver一共有這麼幾種類型

1
class  socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate = True )

This uses the Internet TCP protocol, which provides for continuous streams of data between the client and server. 

1
class  socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate = True )

This uses datagrams, which are discrete packets of information that may arrive out of order or be lost while in transit. The parameters are the same as for TCPServer

1
2
class  socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate = True )
class  socketserver.UnixDatagramServer(server_address, RequestHandlerClass,bind_and_activate = True )

There are five classes in an inheritance diagram, four of which represent synchronous servers of four types:

+------------+ | BaseServer | +------------+ | v +-----------+ +------------------+ | TCPServer |------->| UnixStreamServer | +-----------+ +------------------+ | v +-----------+ +--------------------+ | UDPServer |------->| UnixDatagramServer | +-----------+ +--------------------+


建立一個socketserver 至少分如下幾步:

  1. First, you must create a request handler class by subclassing the BaseRequestHandler class and overriding its handle() method; this method will process incoming requests.   
  2. Second, you must instantiate one of the server classes, passing it the server’s address and the request handler class.
  3. Then call the handle_request() or serve_forever() method of the server object to process one or many requests.
  4. Finally, call server_close() to close the socket.

最基本的socketserver代碼實現

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print(self.data)
                self.request.send(self.data.upper())
            except Exception as e:
                print('連接斷開',e)
                break

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
    server.serve_forever()

讓你的socketserver併發起來, 必須選擇使用如下一個多併發的類

class socketserver.ForkingTCPServer

class socketserver.ForkingUDPServer

class socketserver.ThreadingTCPServer

class socketserver.ThreadingUDPServer

so 只須要把下面這句

server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

換成下面這個,就能夠多併發了,這樣,客戶端每連進一個來,服務器端就會分配一個新的線程來處理這個客戶端的請求

server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)

  固然,知道了socket的實現原理以後,咱們還可使用socket來完成不少事情,好比說用socket來完成一個FTP文件上傳下載的功能也均可以。。。學無止境,慢慢加油吧。

相關文章
相關標籤/搜索