python学习笔记8--socket编程
一、socket
Socket的英文原義是“孔”或“插座”。作為BSD UNIX的進程通信機制,取后一種意思。通常也稱作"套接字",用于描述IP地址和端口,是一個通信鏈的句柄,可以用來實現不同虛擬機或不同計算機之間的通信。在Internet上的主機一般運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,并綁定到一個端口上,不同的端口對應于不同的服務。Socket正如其英文原意那樣,像一個多孔插座。一臺主機猶如布滿各種插座的房間,每個插座有一個編號,有的插座提供220伏交流電, 有的提供110伏交流電,有的則提供有線電視節目。 客戶軟件將插頭插到不同編號的插座,就可以得到不同的服務。
編寫socket程序時需要進過如下步驟:
1、服務端:
a、實例化socket對象:server = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)
參數一:地址簇
socket.AF_INET IPv4(默認)
socket.AF_INET6 IPv6
socket.AF_UNIX 只能夠用于單一的Unix系統進程間通信
參數二:類型
socket.SOCK_STREAM 流式socket , for TCP (默認)
socket.SOCK_DGRAM 數據報式socket , for UDP
socket.SOCK_RAW 原始套接字,普通的套接字無法處理ICMP、IGMP等網絡報文,而SOCK_RAW可以;其次,SOCK_RAW也可以處理特殊的IPv4報文;此外,利用原始套接字,可以通過IP_HDRINCL套接字選項由用戶構造IP頭。
socket.SOCK_RDM 是一種可靠的UDP形式,即保證交付數據報但不保證順序。SOCK_RAM用來提供對原始協議的低級訪問,在需要執行某些特殊操作時使用,如發送ICMP報文。SOCK_RAM通常僅限于高級用戶或管理員運行的程序使用。
socket.SOCK_SEQPACKET 可靠的連續數據包服務
參數三:協議
0 (默認)與特定的地址家族相關的協議,如果是 0 ,則系統就會根據地址格式和套接類別,自動選擇一個合適的協議
b、設置監聽的IP地址和端口號:server.bind(('IP',port))
c、監聽:server.listen()
d、接受連接:conn,addr = server.accept()
e、接受數據:data = server.recv(1024)
1024:這個參數表示一次接收的數據包大小,即一次接收1024字節的包,官方建議最大設置為8192
f、發送數據:server.send(xxxx)
關于發送數據:發送的數據必須是bytes類型,所以在發送之前要對數據進行encode()轉碼
g、關閉連接:server.close()
2、客戶端
a、實例化socket對象:client = socket.socket()
b、連接服務端:client.connect(("server IP",port))
c、發送數據:client.send(xxxx)
d、接收數據:data = client.recv(1024)
e、關閉連接:client.close()
?
二、寫個ssh
我們可以自己寫一個簡單的ssh客戶端程序出來,那么我們一步一步來實現:
1、先寫個簡單的服務端和客戶端
import socketserver = socket.socket() server.bind(("127.0.0.1",9999)) server.listen() conn,addr = server.accept() data = conn.recv(1024) print("接收到了數據:",data.decode()) conn.send("你發來的數據我已經接收到了!".encode("utf-8")) conn.close() 服務端 import socketclient = socket.socket() client.connect(("127.0.0.1",9999)) data = input(">>: ") client.send(data.encode("utf-8")) recevd = client.recv(1024) print("服務器響應信息:",recevd.decode()) client.close() 客戶端運行一下,恩恩,不賴不賴,客戶端真的把數據發出去了,服務端也真的把數據收到了。但是,客戶端發一遍,服務端接一遍,然后程序就都退出了,顯然,這不合理,OK,那我們來加個循環
import socketserver = socket.socket() server.bind(("127.0.0.1",9999)) server.listen() conn,addr = server.accept() while True:data = conn.recv(1024)print("接收到了數據:",data.decode())conn.send("你發來的數據我已經接收到了!".encode("utf-8")) conn.close() 服務端1.0 import socketclient = socket.socket() client.connect(("127.0.0.1",9999)) while True:data = input(">>: ")client.send(data.encode("utf-8"))recevd = client.recv(1024)print("服務器響應信息:",recevd.decode()) client.close() 客戶端1.0再運行一下,很好,已經可以不斷的收發數據了,接下來我們讓服務端執行客戶端的命令,并把命令結果返回
import socket,osserver = socket.socket() server.bind(("127.0.0.1",9999)) server.listen() conn,addr = server.accept() while True:print("準備好接收數據了")data = conn.recv(1024)print("接收到了命令:",data.decode())cmd_res = os.popen(data.decode()).read()conn.send("你發來的數據我已經接收到了!".encode("utf-8"))conn.send(cmd_res.encode("utf-8")) conn.close() 服務端2.0 import socketclient = socket.socket() client.connect(("127.0.0.1",9999)) while True:data = input(">>: ")client.send(data.encode("utf-8"))recevd = client.recv(1024)print("服務器響應信息:",recevd.decode())cmd_res = client.recv(1024)print(cmd_res.decode()) client.close() 客戶端2.0運行一下,發現還不錯,常規的命令,像ls什么的都可以了,但是,有很多蛋疼的地方:
輸入空命令或錯誤命令,客戶端和服務端都會卡住
一個服務端只能為一個客戶端提供服務,客戶端退出,服務端也退出
針對這些問題,我們再做一些改進:
import socket,osserver = socket.socket() server.bind(("127.0.0.1",9999)) server.listen() while True:conn,addr = server.accept()while True:print("準備好接收數據了")data = conn.recv(1024)if not data:breakprint("接收到了命令:",data.decode())cmd_res = os.popen(data.decode()).read()if len(cmd_res) == 0:conn.send("命令輸入有誤".encode("utf-8"))else:conn.send(cmd_res.encode("utf-8")) conn.close() 服務端3.0 import socketclient = socket.socket() client.connect(("127.0.0.1",9999)) while True:data = input(">>: ").strip()if len(data) == 0:continueclient.send(data.encode("utf-8"))cmd_res = client.recv(1024)print(cmd_res.decode()) client.close() 客戶端3.0? 到目前為止,一切都看起來很不錯了,客戶端輸入空命令,錯誤命令都不會有任何問題,但是還有個問題,當服務端發送的數據量大于1024時,悲劇了,客戶端不會一次接收完,而是在客戶端向服務端發送了新的請求后,服務端才吧上次沒發完的數據發送過去,這問題就嚴重了,那應該怎么解決呢?我們想,客戶端如果能提前知道服務端要發多少數據,那客戶端就可以決定接收多少次,以此來保證數據全部接收完成。思路正確,那我們就讓服務端在發送正常數據之前,先向客戶端發送一下要發送的數據的大小:
import socket,osserver = socket.socket() server.bind(("127.0.0.1",9999)) server.listen() while True:conn,addr = server.accept()while True:print("準備好接收數據了")data = conn.recv(1024)if not data:breakprint("接收到了命令:",data.decode())cmd_res = os.popen(data.decode()).read()if len(cmd_res) == 0:conn.send("命令輸入有誤".encode("utf-8"))else:send_len = len(cmd_res.encode("utf-8"))conn.send(str(send_len).encode("utf-8"))conn.send(cmd_res.encode("utf-8")) conn.close() 服務端4.0? import socketclient = socket.socket() client.connect(("127.0.0.1",9999)) while True:data = input(">>: ").strip()if len(data) == 0:continueclient.send(data.encode("utf-8"))len_cmd_res = client.recv(1024).decode()received_len = 0data_rec = b""while received_len < int(len_cmd_res):cmd_res = client.recv(1024)data_rec += cmd_resreceived_len += len(cmd_res)print(data_rec.decode()) client.close() 客戶端4.0? 我們運行上述代碼,然后查看一些較大的文件,我們可以看到,此時客戶端就可以一次性把所有的數據都接收到了,但是多試幾次就會發現一個問題,會有報錯如下:
Traceback (most recent call last):File "/Users/zhanghaoyan/PycharmProjects/day08/pritice_client.py", line 15, in <module>while received_len < int(len_cmd_res): ValueError: invalid literal for int() with base 10: '1366一、動態導入模塊\n\timport importlib\n\n\tmod = ........? 從報錯的信息可以看出,服務端在兩次發送數據的過程中并沒有將數據長度和數據分開發送,而是一起發送給了客戶端,而且這種現象不是一直存在,而是偶爾就出現一次,這種現象稱之為?粘包 ,為什么會出現粘包現象呢?
出現粘包現象的原因是多方面的,它既可能由發送方造成,也可能由接收方造成。發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一包數據。若連續幾次發送的數據都很少,通常TCP會根據優化算法把這些數據合成一包后一次發送出去,這樣接收方就收到了粘包數據。接收方引起的粘包是由于接收方用戶進程不及時接收數據,從而導致粘包現象。這是因為接收方先把收到的數據放在系統接收緩沖區,用戶進程從該緩沖區取數據,若下一包數據到達時前一包數據尚未被用戶進程取走,則下一包數據放到系統接收緩沖區時就接到前一包數據之后,而用戶進程根據預先設定的緩沖區大小從系統接收緩沖區取數據,這樣就一次取到了多包數據。
既然出現了粘包,那我們就需要想方設法避免這個問題,那如何避免呢?著手點有兩個:
1、利用TCP/IP協議發包的超時時間,即超時后強制發包
2、另一種是將兩個發包過程分開來,這樣就不會出現粘包現象了?
? 根據第一種攝像,我們可以讓兩次發包過程中間等待一個時間,這樣就能避免粘包:
import socket,os,timeserver = socket.socket() server.bind(("127.0.0.1",9999)) server.listen() while True:conn,addr = server.accept()while True:print("準備好接收數據了")data = conn.recv(1024)if not data:breakprint("接收到了命令:",data.decode())cmd_res = os.popen(data.decode()).read()if len(cmd_res) == 0:conn.send("命令輸入有誤".encode("utf-8"))else:send_len = len(cmd_res.encode("utf-8"))conn.send(str(send_len).encode("utf-8"))time.sleep(0.5)conn.send(cmd_res.encode("utf-8")) conn.close()我們運行代碼可以看到,這樣做以后就完全沒有粘包現象出現了,但是程序執行上確多了一個0.5秒的耗時,單次執行的時候這0.5秒似乎并不是什么問題,但是,如果程序很大,頻繁的發包呢?那這個時間成本將會變得非常高,手動拉低程序運行速度這種事顯然是與社會主義和諧社會背道而馳的,我們不能這么干。
既然第一種方案被我們否決了,那第二種方案該怎么實現呢?我們怎么樣讓兩次發包的過程分開而又不增加過多的消耗呢?其實我們可以在兩次發包中間在加入一個接收客戶端請求的動作,然后讓客戶端在收到第一個數據包后發送一個確認信號,然后服務端再進行接下來的發送:
服務端:
import socket,os,timeserver = socket.socket() server.bind(("127.0.0.1",9999)) server.listen() while True:conn,addr = server.accept()while True:print("準備好接收數據了")data = conn.recv(1024)if not data:breakprint("接收到了命令:",data.decode())cmd_res = os.popen(data.decode()).read()if len(cmd_res) == 0:conn.send("命令輸入有誤".encode("utf-8"))else:send_len = len(cmd_res.encode("utf-8"))conn.send(str(send_len).encode("utf-8"))ack = conn.recv(1024)conn.send(cmd_res.encode("utf-8")) conn.close()?
? 客戶端:
import socketclient = socket.socket() client.connect(("127.0.0.1",9999)) while True:data = input(">>: ").strip()if len(data) == 0:continueclient.send(data.encode("utf-8"))len_cmd_res = client.recv(1024).decode()client.send(b"OK")received_len = 0data_rec = b""while received_len < int(len_cmd_res):cmd_res = client.recv(1024)data_rec += cmd_resreceived_len += len(cmd_res)print(data_rec.decode()) client.close()?
? OK!Well done!我們的ssh簡單版算是制作完成了,但是這個代碼還沒法執行top這樣的命令,原因在于我們的命令需要執行結束后,才能將結果返回,關于這點,以后再說吧。
?三、socketserver
SocketServer內部使用 IO多路復用 以及 “多線程” 和 “多進程” ,從而實現并發處理多個客戶端請求的Socket服務端。即:每個客戶端請求連接到服務器時,Socket服務端都會在服務器是創建一個“線程”或者“進程” 專門負責處理當前客戶端的所有請求。
socketserver模塊有以下幾種類型:
socketserver.BaseServer():BaseServer不直接對外服務。
socketserver.TCPServer():用于使用TCP協議的連接
socketserver.UDPServer():用于使用UDP協議的連接
socketserver.UnixStreamServer():只在Unix環境下使用
socketserver.UnixDatagramServer():只在Unix環境下使用
創建一個socketserver需要以下幾步:
1、創建一個類,該類繼承socketserver.BaseRequestHandler并重構該類的handle()方法;
2、選擇使用一個類型來實例化一個對象,并將(IP地址,端口號),上述類名作為參數傳入
3、調用handle_request()(一般是調用其他事件循環或者使用select())或serve_forever()
4、調用server_close()關閉server
注意:讓你的socketserver并發起來, 必須選擇使用以下一個多并發的類
class?socketserver.ForkingTCPServer
class?socketserver.ForkingUDPServer
class?socketserver.ThreadingTCPServer
class?socketserver.ThreadingUDPServer
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import socketserverclass Mysocket(socketserver.BaseRequestHandler):def handle(self):while True:try:self.date = self.request.recv(1024)print("接收到了:",self.date)self.request.send(self.date.upper())except ConnectionResetError as e:print("error:",e)breakif __name__ == "__main__":HOST,PORT = "0.0.0.0",9999obj = socketserver.TCPServer((HOST,PORT),Mysocket)obj.serve_forever() Socketserver-Server #!/usr/bin/env python3 # -*- coding:utf-8 -*-import socketclient = socket.socket() client.connect(("localhost",9999)) while True:cmd = input(">>:").strip()client.send(cmd.encode("utf-8"))date_rec_len = client.recv(1024)print(date_rec_len.decode()) Socketserver-Client如果想支持并發,那就將上述server端代碼中的?obj = socketserver.TCPServer((HOST,PORT),Mysocket) 換成?obj = socketserver.ThreadingTCPServer((HOST,PORT),Mysocket)就可以了。
接下來我們來看下BaseServer中有哪些方法吧:
1 BaseServer.fileno():返回服務器監聽套接字的整數文件描述符。通常用來傳遞給select.select(), 以允許一個進程監視多個服務器。 2 3 BaseServer.handle_request():處理單個請求。處理順序:get_request(), verify_request(), process_request()。如果用戶提供handle()方法拋出異常,將調用服務器的handle_error()方法。如果self.timeout內沒有請求收到, 將調用handle_timeout()并返回handle_request()。 4 5 BaseServer.serve_forever(poll_interval=0.5): 處理請求,直到一個明確的shutdown()請求。每poll_interval秒輪詢一次shutdown。忽略self.timeout。如果你需要做周期性的任務,建議放置在其他線程。 6 7 BaseServer.shutdown():告訴serve_forever()循環停止并等待其停止。python2.6版本。 8 9 BaseServer.address_family: 地址家族,比如socket.AF_INET和socket.AF_UNIX。 10 11 BaseServer.RequestHandlerClass:用戶提供的請求處理類,這個類為每個請求創建實例。 12 13 BaseServer.server_address:服務器偵聽的地址。格式根據協議家族地址的各不相同,請參閱socket模塊的文檔。 14 15 BaseServer.socketSocket:服務器上偵聽傳入的請求socket對象的服務器。 16 17 服務器類支持下面的類變量: 18 19 BaseServer.allow_reuse_address:服務器是否允許地址的重用。默認為false ,并且可在子類中更改。 20 21 BaseServer.request_queue_size:請求隊列的大小。如果單個請求需要很長的時間來處理,服務器忙時請求被放置到隊列中,最多可以放request_queue_size個。一旦隊列已滿,來自客戶端的請求將得到 “Connection denied”錯誤。默認值通常為5 ,但可以被子類覆蓋。 22 23 BaseServer.socket_type:服務器使用的套接字類型; socket.SOCK_STREAM和socket.SOCK_DGRAM等。 24 25 BaseServer.timeout:超時時間,以秒為單位,或 None表示沒有超時。如果handle_request()在timeout內沒有收到請求,將調用handle_timeout()。 26 27 下面方法可以被子類重載,它們對服務器對象的外部用戶沒有影響。 28 29 BaseServer.finish_request():實際處理RequestHandlerClass發起的請求并調用其handle()方法。 常用。 30 31 BaseServer.get_request():接受socket請求,并返回二元組包含要用于與客戶端通信的新socket對象,以及客戶端的地址。 32 33 BaseServer.handle_error(request, client_address):如果RequestHandlerClass的handle()方法拋出異常時調用。默認操作是打印traceback到標準輸出,并繼續處理其他請求。 34 35 BaseServer.handle_timeout():超時處理。默認對于forking服務器是收集退出的子進程狀態,threading服務器則什么都不做。 36 37 BaseServer.process_request(request, client_address) :調用finish_request()創建RequestHandlerClass的實例。如果需要,此功能可以創建新的進程或線程來處理請求,ForkingMixIn和ThreadingMixIn類做到這點。常用。 38 39 BaseServer.server_activate():通過服務器的構造函數來激活服務器。默認的行為只是監聽服務器套接字??芍剌d。 40 41 BaseServer.server_bind():通過服務器的構造函數中調用綁定socket到所需的地址??芍剌d。 42 43 BaseServer.verify_request(request, client_address):返回一個布爾值,如果該值為True ,則該請求將被處理,反之請求將被拒絕。此功能可以重寫來實現對服務器的訪問控制。默認的實現始終返回True。client_address可以限定客戶端,比如只處理指定ip區間的請求。 常用。 BaseServer相關方法和屬性?
轉載于:https://www.cnblogs.com/crafts-zhang/articles/5866024.html
總結
以上是生活随笔為你收集整理的python学习笔记8--socket编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 什么是视频降噪
- 下一篇: python操作SQL