socket編程javascript
Python 提供了兩個級別訪問的網絡服務。java
- 低級別的網絡服務支持基本的 Socket,它提供了標準的 BSD Sockets API,能夠訪問底層操做系統Socket接口的所有方法。
- 高級別的網絡服務模塊 SocketServer, 它提供了服務器中心類,能夠簡化網絡服務器的開發。
Socket是什麼 ?
- Socket又稱"套接字",應用程序一般經過"套接字"向網絡發出請求或者應答網絡請求,使主機間或者一臺計算機上的進程間能夠通信。
socket()函數
- Python 中,咱們用 socket()函數來建立套接字,語法格式以下:
1
2
|
import
socket
socket.socket([family[,
type
[, proto]]])
|
- 參數
family: 套接字家族可使AF_UNIX或者AF_INETpython
type: 套接字類型能夠根據TCP(面向鏈接)的仍是UDP(非鏈接)分爲SOCK_STREAM
或SOCK_DGRAM
linux
protocol: 通常不填默認爲0.shell
import socket socket.socket(socket_family,socket_type,protocal=0) socket_family 能夠是 AF_UNIX 或 AF_INET。socket_type 能夠是 SOCK_STREAM 或 SOCK_DGRAM。protocol 通常不填,默認值爲 0。 獲取tcp/ip套接字 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 獲取udp/ip套接字 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 因爲 socket 模塊中有太多的屬性。咱們在這裏破例使用了'from module import *'語句。使用 'from socket import *',咱們就把 socket 模塊裏的全部屬性都帶到咱們的命名空間裏了,這樣能 大幅減短咱們的代碼。 例如tcpSock = socket(AF_INET, SOCK_STREAM)
- socket對象(內建)方法
服務器端套接字 | |
s.bind() | 綁定地址(host,port)到套接字, 在AF_INET下,以元組(host,port)的形式表示地址。 |
s.listen() | 開始TCP監聽。backlog指定在拒絕鏈接以前,操做系統能夠掛起的最大鏈接數量。該值至少爲1,大部分應用程序設爲5就能夠了。 |
s.accept() | 被動接受TCP客戶端鏈接,(阻塞式)等待鏈接的到來 |
客戶端套接字 | |
s.connect() | 主動初始化TCP服務器鏈接,。通常address的格式爲元組(hostname,port),若是鏈接出錯,返回socket.error錯誤。 |
s.connect_ex() | connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常 |
公共用途的套接字函數 | |
s.recv() | 接收TCP數據,數據以字符串形式返回,bufsize指定要接收的最大數據量。flag提供有關消息的其餘信息,一般能夠忽略。 |
s.send() | 發送TCP數據,將string中的數據發送到鏈接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。 |
s.sendall() | 完整發送TCP數據,完整發送TCP數據。將string中的數據發送到鏈接的套接字,但在返回以前會嘗試發送全部數據。成功返回None,失敗則拋出異常。 |
s.recvform() | 接收UDP數據,與recv()相似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。 |
s.sendto() | 發送UDP數據,將數據發送到套接字,address是形式爲(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。 |
s.close() | 關閉套接字 |
s.getpeername() | 返回鏈接套接字的遠程地址。返回值一般是元組(ipaddr,port)。 |
s.getsockname() | 返回套接字本身的地址。一般是一個元組(ipaddr,port) |
s.setsockopt(level,optname,value) | 設置給定套接字選項的值。 |
s.getsockopt(level,optname[.buflen]) | 返回套接字選項的值。 |
s.settimeout(timeout) | 設置套接字操做的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。通常,超時期應該在剛建立套接字時設置,由於它們可能用於鏈接的操做(如connect()) |
s.gettimeout() | 返回當前超時期的值,單位是秒,若是沒有設置超時期,則返回None。 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) | 若是flag爲0,則將套接字設爲非阻塞模式,不然將套接字設爲阻塞模式(默認值)。非阻塞模式下,若是調用recv()沒有發現任何數據,或send()調用沒法當即發送數據,那麼將引發socket.error異常。 |
s.makefile() | 建立一個與該套接字相關連的文件 |
辣麼,如今能夠搭建一個簡單的服務端與客戶端之間的通訊編程
基於TCP的套接字windows
服務端服務器
咱們使用 socket 模塊的 socket 函數來建立一個 socket 對象。socket 對象能夠經過調用其餘函數來設置一個 socket 服務。cookie
如今咱們能夠經過調用 bind(hostname, port) 函數來指定服務的 port(端口)。網絡
接着,咱們調用 socket 對象的 accept 方法。該方法等待客戶端的鏈接,並返回 connection 對象,表示已鏈接到客戶端。
#!/usr/bin/python3 # 文件名:server.py # 導入 socket模塊 import socket # 建立 socket 對象 serversocket = socket.socket( socket.AF_INET, socket.SOCK_STREAM) # 獲取本地主機名 host = socket.gethostname() port = 8080 # 綁定端口 serversocket.bind((host, port)) # 設置最大鏈接數,超事後排隊 serversocket.listen(5) while True: # 創建客戶端鏈接 clientsocket,addr = serversocket.accept() print("鏈接地址: %s" % str(addr)) msg='welcome to JinCheng!'+ "\r\n" clientsocket.send(msg.encode('utf-8')) clientsocket.close()
客戶端
接下來咱們寫一個簡單的客戶端實例鏈接到以上建立的服務。
socket.connect(hosname, port ) 方法打開一個 TCP 鏈接到主機爲 hostname 端口爲 port 的服務商。鏈接後咱們就能夠從服務端後期數據,記住,操做完成後須要關閉鏈接。
#!/usr/bin/python3 # 文件名:client.py
# 導入 socket模塊 import socket # 建立 socket 對象 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 獲取本地主機名 host = socket.gethostname() # 設置端口好 port = 8080 # 鏈接服務,指定主機和端口 s.connect((host, port)) # 接收小於 1024 字節的數據 msg = s.recv(1024) s.close() print (msg.decode('utf-8'))
上面的服務端與客戶端實現了"本身"與"本身"之間的簡單通訊
辣麼,基於上述也能夠實現一種模擬現實"打電話"的通訊
服務端
咱們想實現打電話的通訊,首先服務端要不斷接受鏈接,而後循環通訊,實現一種交互式的收發,通訊完畢後只關閉連接,服務器可以繼續接收下一次連接
import socket ip_port = ("127.0.0.1",8080)# 電話卡 back_log = 5 buffer_size = 1024 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#買手機 s.bind(ip_port) #手機插卡 s.listen(back_log)#手機待機 while True: #新增接收連接循環,能夠不停的接電話 conn,address = s.accept()#手機接電話 # print(conn) print('接到來自%s的電話' %address) while True:#新增通訊循環,能夠不斷的通訊,收發消息 try: #新增捕捉異常的,若是不加,那麼正在連接的客戶端忽然斷開,recv便再也不阻塞,死循環發生 ret = str(conn.recv(buffer_size),encoding="utf-8") print("from>>> " ,ret) #收到來自於客戶端的消息 nr = input(">>>") #交互式發送,發給客戶端消息 conn.sendall(bytes(nr, encoding="utf-8")) except Exception: break #conn.close() #掛電話(可選) #s.close() #手機關機(可選)
客戶端
import socket ip_port = ("127.0.0.1",8080) buffer_size = 1024 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)# 買手機 s.connect(ip_port) # 撥電話 while True: # 新增通訊循環,客戶端能夠不斷髮收消息 nr = input(">>>").strip() # if not nr:continue # 若是輸入爲空,則返回循環 s.sendall(bytes(nr, encoding="utf-8"))# 發消息,說話(只能發送字節類型) res = s.recv(buffer_size) # 收消息,聽話 print("來自遠方的消息",str(res, encoding="utf-8")) #s.close() # 掛電話(可選)
基於上次例子可能會遇到一些問題:
這個是因爲你的服務端仍然存在四次揮手的time_wait狀態在佔用地址(若是不懂,請深刻研究1.tcp三次握手,四次揮手 2.syn洪水攻擊 3.服務器高併發狀況下會有大量的time_wait狀態的優化方法)
解決方案:
#加入一條socket配置,重用ip和端口 phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) #此條能夠解決上述的問題
發現系統存在大量TIME_WAIT狀態的鏈接,經過調整linux內核參數解決, vi /etc/sysctl.conf 編輯文件,加入如下內容: net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 而後執行 /sbin/sysctl -p 讓參數生效。 net.ipv4.tcp_syncookies = 1 表示開啓SYN Cookies。當出現SYN等待隊列溢出時,啓用cookies來處理,可防範少許SYN攻擊,默認爲0,表示關閉; net.ipv4.tcp_tw_reuse = 1 表示開啓重用。容許將TIME-WAIT sockets從新用於新的TCP鏈接,默認爲0,表示關閉; net.ipv4.tcp_tw_recycle = 1 表示開啓TCP鏈接中TIME-WAIT sockets的快速回收,默認爲0,表示關閉。 net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間 # 看不懂略過,我也看不懂
注:若是有倆個客戶端訪問服務端,則優先處理第一個訪問的客戶端,另一個則被放在鏈接池中等待。當第一個客戶端強行終止與服務端的鏈接,服務端觸發異常,新增一個捕捉異常的功能,能夠將後面的鏈接池中的客戶端再次創建通訊鏈接。
辣麼,基於上述也能夠模擬實現TCP遠程執行命令
# 基於tcp協議實現的遠程鏈接命令工做 from socket import * import subprocess ip_port = ("127.0.0.1",8080) back_log = 5 buffer_size = 1024 tcp_server = socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) while True: conn,addr = tcp_server.accept() print("客戶端鏈接",addr) while True: try: cmd = conn.recv(buffer_size) if not cmd:break print("收到客戶端的命令是:",cmd) res = subprocess.Popen(cmd.decode("utf-8"),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err = res.stderr.read() if err: cmd_res = err else: cmd_res = res.stdout.read() if not cmd_res: cmd_res = "命令執行OK".encode("gbk") conn.send(cmd_res) except Exception as e: print(e) break conn.close()
from socket import * import subprocess ip_port = ("127.0.0.1",8080) buffer_size = 1024 tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: msg = input(">>>") if not msg:continue if msg == "q":break #若是輸入的是q 則發送的是空 服務端會進入死循環 tcp_client.send(msg.encode("utf-8")) cmd_res = tcp_client.recv(buffer_size) print("命令行的結果是",cmd_res.decode("gbk")) tcp_client.close()
基於UDP的套接字
udp的socket修改了一些參數
服務端
from socket import * udp_server = socket(AF_INET,SOCK_DGRAM) # 基於網絡, 數據報 udp_server.bind(("127.0.0.1",8080,)) while True: data,addr = udp_server.recvfrom(1024) print("來自於:",addr,data.decode("utf-8")) msg = input(">>>") udp_server.sendto(msg.encode("utf-8"),addr)
客戶端
from socket import * ip_port=("192.168.12.34",8899) udp_client = socket(AF_INET,SOCK_DGRAM) # 基於網絡, 數據報 while True: msg = input(">>>") udp_client.sendto(msg.encode("utf-8"),ip_port) # 發送於誰 data,addr=udp_client.recvfrom(1024) # 接收來自於誰 print(data.decode("utf-8"))
對UDP進行的一些闡釋 若是發送的內容爲空,例如直接回車,不會阻塞。在TCP下按下回車則會阻塞,則要做出一些處理。UDP不會出現粘包現象,再講解粘包時會做出解釋。並且UDP能夠直接實現併發,支持多用戶同時通訊服務端
基於udp套接字咱們能夠做出一些小功能
實現ntp時間服務器
from socket import * import time udp_server = socket(AF_INET,SOCK_DGRAM) # 基於網絡, 數據報 udp_server.bind(("127.0.0.1",8080,)) while True: data,addr = udp_server.recvfrom(1024) print("來自於:",addr,data.decode("utf-8")) if not data: gs = "%F %X" else: gs = data.decode("utf-8") sj = time.strftime(gs) udp_server.sendto(sj.encode("utf-8"),addr)
from socket import * ip_port=("127.0.0.1",8080) udp_client = socket(AF_INET,SOCK_DGRAM) # 基於網絡, 數據報 while True: msg = input(">>>") udp_client.sendto(msg.encode("utf-8"),ip_port) # 發送於誰 data,addr=udp_client.recvfrom(1024) # 接收來自於誰 print("標準時間爲:",data.decode("utf-8"))
基於udp套接字模擬實現UDP遠程執行命令
# 基於udp實現的遠程命令行 from socket import * import subprocess ip_port = ("127.0.0.1",8080) buffer_size = 1024 udp_server = socket(AF_INET,SOCK_DGRAM) udp_server.bind(ip_port) while True: # 收 cmd,addr = udp_server.recvfrom(buffer_size) print("收到客戶端的命令是:", cmd) res = subprocess.Popen(cmd.decode("utf-8"),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err = res.stderr.read() if err: cmd_res = err else: cmd_res = res.stdout.read() if not cmd_res: # 若是這個命令的返回值是空的,則會阻塞.因此要人爲的添加一些操做 cmd_res = "ok".encode("utf-8") # 發 udp_server.sendto(cmd_res,addr)
from socket import * import subprocess ip_port = ("127.0.0.1",8080) buffer_size = 1024 udp_client = socket(AF_INET,SOCK_DGRAM) while True: msg = input(">>>") udp_client.sendto(msg.encode("utf-8"), ip_port) data,addr = udp_client.recvfrom(buffer_size) print("命令的結果是" ,data.decode("gbk"))
注:tcp和udp實現的遠程執行命令由於系統的分別,可能會產生一些差別性。在windows上面,由於將命令交給shell去操做,因此shell底層是用gbk編碼的,再獲得shell處理返回的結果時,則要用gbk去解碼拿到結果。
關於tcp的recv和udp的recvfrom的一些區別
一、收發原理詳解:
發消息:都是將數據發送到己端的發送緩衝區中
收消息:都是從己端的緩衝區中收
二、發消息兩者相似,收消息確實有區別的?
tcp協議:send發消息,recv收消息
(1)若是收消息緩衝區裏的數據爲空,那麼recv就會阻塞
(2)tcp基於連接通訊,若是一端斷開了連接,那另一端的連接也跟着完蛋recv將不會阻塞,收到的是空
udp協議:sendto發消息,recvfrom收消息
(1)若是若是收消息緩衝區裏的數據爲「空」,recvfrom不會阻塞
(2)recvfrom收的數據小於sendinto發送的數據時,數據丟失
(3)只有sendinto發送數據沒有recvfrom收數據,數據丟失
注意:
1.你單獨運行上面的udp的客戶端,你發現並不會報錯,相反tcp卻會報錯,由於udp協議只負責把包發出去,對方收不收,根本無論,而tcp是基於連接的,必須有一個服務端先運行着,客戶端去跟服務端創建連接而後依託於連接才能傳遞消息,任何一方試圖把連接摧毀都會致使對方程序的崩潰。
2.上面的udp程序,你註釋任何一條客戶端的sendinto,服務端都會卡住,爲何?由於服務端有幾個recvfrom就要對應幾個sendinto,哪怕是sendinto(b'')那也要有。
總結:
1.udp的sendinto不用管是否有一個正在運行的服務端,能夠己端一個勁的發消息
2.udp的recvfrom是阻塞的,一個recvfrom(x)必須對一個一個sendinto(y),收完了x個字節的數據就算完成,如果y>x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠
3.tcp的協議數據不會丟,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。