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