本文轉載自大王http://www.cnblogs.com/alex3714/articles/5830365.html
加有自己的注釋,應該會比原文更突出重點些
一. 基本Socket實例
前面講了這么多,到底咋么用呢?
1 import socket
2
3 server = socket.socket() #獲得socket實例
4
5 server.bind(("localhost",9998)) #綁定ip port
6 server.listen() #開始監聽
7 print("等待客戶端的連接...")
8 conn,addr = server.accept() #接受並建立與客戶端的連接,程序在此處開始阻塞,只到有客戶端連接進來...
9 print("新連接:",addr )
10
11 data = conn.recv(1024)
12 print("收到消息:",data)
13
14
15 server.close()
1 import socket
2
3 client = socket.socket()
4
5 client.connect(("localhost",9998))
6
7 client.send(b"hey")
8
9 client.close()
上面的代碼的有一個問題, 就是SocketServer.py運行起來后, (問題一)接收了一次客戶端的data就退出了。。。, 但實際場景中,一個連接建立起來后,可能要進行多次往返的通信。

(改進一)多次的數據交互怎么實現呢?
1 import socket
2
3 server = socket.socket() #獲得socket實例
4
5 server.bind(("localhost",9998)) #綁定ip port
6 server.listen() #開始監聽
7 print("等待客戶端的連接...")
8 conn,addr = server.accept() #接受並建立與客戶端的連接,程序在此處開始阻塞,只到有客戶端連接進來...
9 print("新連接:",addr )
10 while True:
11
12 data = conn.recv(1024)
13
14 print("收到消息:",data)
15 conn.send(data.upper())
16
17 server.close()
1 import socket
2
3 client = socket.socket()
4
5 client.connect(("localhost",9998))
6
7 while True:
8 msg = input(">>:").strip()
9 if len(msg) == 0:continue
10 client.send( msg.encode("utf-8") )
11
12 data = client.recv(1024)
13 print("來自服務器:",data)
14
15 client.close()
實現了多次交互, 棒棒的, 但你會(問題二)發現一個小問題, 就是客戶端一斷開,服務器端就進入了死循環(我自己測試Windows不會, Linux會),為啥呢?
看客戶端斷開時服務器端的輸出
|
1
2
3
4
5
6
7
8
9
|
等待客戶端的連接...
新連接: (
'127.0.0.1'
,
62722
)
收到消息: b
'hey'
收到消息: b
'you'
收到消息: b''
#客戶端一斷開,服務器端就收不到數據了,但是不會報錯,就進入了死循環模式。。。
收到消息: b''
收到消息: b''
收到消息: b''
收到消息: b''
|
知道了原因就好解決了,只需要(改進二)加個判斷服務器接到的數據是否為空就好了,為空就代表斷了。。。
1 import socket
2
3 server = socket.socket() #獲得socket實例
4
5 server.bind(("localhost",9998)) #綁定ip port
6 server.listen() #開始監聽
7 print("等待客戶端的連接...")
8 conn,addr = server.accept() #接受並建立與客戶端的連接,程序在此處開始阻塞,只到有客戶端連接進來...
9 print("新連接:",addr )
10 while True:
11
12 data = conn.recv(1024)
13 if not data:
14 print("客戶端斷開了...")
15 break
16 print("收到消息:",data)
17 conn.send(data.upper())
18
19 server.close()
注意:
python3對這個進行了改進,無論是windows還是Linux都是異常ConnectionResetError

二.Socket實現多連接處理
上面的代碼雖然實現了服務端與客戶端的多次交互,但是你會發現,(問題三)如果客戶端斷開了, 服務器端也會跟着立刻斷開,因為服務器只有一個while 循環,客戶端一斷開,服務端收不到數據 ,就會直接break跳出循環,然后程序就退出了,這顯然不是我們想要的結果 ,我們想要的是,客戶端如果斷開了,我們這個服務端還可以為下一個客戶端服務,它不能斷,她接完一個客,擦完嘴角的遺留物,就要接下來勇敢的去接待下一個客人。 在這里如何實現呢?
|
1
|
conn,addr
=
server.accept()
#接受並建立與客戶端的連接,程序在此處開始阻塞,只到有客戶端連接進來...
|
我們知道上面這句話負責等待並接收新連接,對於上面那個程序,其實在(改進三)while break之后,只要讓程序再次回到上面這句代碼這,就可以讓服務端繼續接下一個客戶啦。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import
socket
server
=
socket.socket()
#獲得socket實例
server.bind((
"localhost"
,
9998
))
#綁定ip port
server.listen()
#開始監聽
while
True
:
#第一層loop
print
(
"等待客戶端的連接..."
)
conn,addr
=
server.accept()
#接受並建立與客戶端的連接,程序在此處開始阻塞,只到有客戶端連接進來...
print
(
"新連接:"
,addr )
while
True
:
data
=
conn.recv(
1024
)
if
not
data:
print
(
"客戶端斷開了..."
)
break
#這里斷開就會再次回到第一次外層的loop
print
(
"收到消息:"
,data)
conn.send(data.upper())
server.close()
|
注意了, 此時(問題四,未改進哈哈,以后學到再發blog)服務器端依然只能同時為一個客戶服務,其客戶來了,得排隊(連接掛起),不能玩 three some. 這時你說想,我就想玩3p,就想就想嘛,其實也可以,多交錢嘛,繼續往下看,后面開啟新姿勢后就可以玩啦。。。
三.通過socket實現簡單的ssh
光只是簡單的發消息、收消息沒意思,干點正事,可以做一個極簡版的ssh,就是客戶端連接上服務器后,讓服務器執行命令,並返回結果給客戶端。
import socket
import os
server = socket.socket() #獲得socket實例
#server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("localhost",9998)) #綁定ip port
server.listen() #開始監聽
while True: #第一層loop
print("等待客戶端的連接...")
conn,addr = server.accept() #接受並建立與客戶端的連接,程序在此處開始阻塞,只到有客戶端連接進來...
print("新連接:",addr )
while True:
data = conn.recv(1024)
if not data:
print("客戶端斷開了...")
break #這里斷開就會再次回到第一次外層的loop
print("收到命令:",data)
res = os.popen(data.decode()).read() #py3 里socket發送的只有bytes,os.popen又只能接受str,所以要decode一下
print(len(res))
conn.send(res.encode("utf-8"))
server.close()
import socket
client = socket.socket()
client.connect(("localhost",9998))
while True:
msg = input(">>:").strip()
if len(msg) == 0:continue
client.send( msg.encode("utf-8") )
data = client.recv(1024)
print(data.decode()) #命令執行結果
client.close()
very cool , 這樣我們就做了一個簡單的ssh , 但多試幾條命令你就會發現,上面的程序有以下2個問題。
- 不能執行top等類似的 會持續輸出的命令,這是因為,服務器端在收到客戶端指令后,會一次性通過os.popen執行,並得到結果后返回給客戶,但(問題五,未改進哈哈,以后學到再發blog)top這樣的命令用os.popen執行你會發現永遠都不會結束,所以客戶端也永遠拿不到返回。(真正的ssh是通過select 異步等模塊實現的,我們以后會涉及)
- 不能執行像cd這種沒有返回的指令, 因為客戶端每發送一條指令,就會通過client.recv(1024)等待接收服務器端的返回結果,但是(問題六)cd命令沒有結果 ,服務器端調用conn.send(data)時是不會發送數據給客戶端的。 所以客戶端就會一直等着,等到天荒地老,結果就卡死了。解決的辦法是,(改進六)在服務器端判斷命令的執行返回結果的長度,如果結果為空,就自己加個結果返回給客戶端,如寫上"cmd exec success, has no output."
- 如果執行的命令(問題七)返回結果的數據量比較大,會發現,結果返回不全,在客戶端上再執行一條命令,結果返回的還是上一條命令的后半段的執行結果,這是為什么呢?這是因為,我們的客戶寫client.recv(1024), 即客戶端一次最多只接收1024個字節,如果服務器端返回的數據是2000字節,那有至少9百多字節是客戶端第一次接收不了的,那怎么辦呢,服務器端此時不能把數據直接扔了呀,so它會暫時存在服務器的io發送緩沖區里,等客戶端下次再接收數據的時候再發送給客戶端。 這就是為什么客戶端執行第2條命令時,卻接收到了第一條命令的結果的原因。 這時有同學說了, 那我直接在客戶端把client.recv(1024)改大一點不就好了么, 改成一次接收個100mb,哈哈,這是不行的,因為socket每次接收和發送都有最大數據量限制的,畢竟網絡帶寬也是有限的呀,不能一次發太多,發送的數據最大量的限制 就是緩沖區能緩存的數據的最大量,這個緩沖區的最大值在不同的系統上是不一樣的, 我實在查不到一個具體的數字,但測試的結果是,在linux上最大一次可接收10mb左右的數據,不過官方的建議是不超過8k,也就是8192,並且數據要可以被2整除,不要問為什么 。anyway , 如果一次只能接收最多不超過8192的數據 ,那服務端返回的數據超過了這個數字怎么辦呢?比如讓服務器端打開一個5mb的文件並返回,客戶端怎么才能完整的接受到呢?那就只能(改進七)循環收取啦。
在開始解決上面問題3之前,我們要考慮,客戶端要循環接收服務器端的大量數據返回直到一條命令的結果全部返回為止, 但問題是客戶端知道服務器端返回的數據有多大么?答案是不知道,那既然不知道服務器的要返回多大的數據,那客戶端怎么知道要循環接收多少次呢?答案是不知道,擦,那咋辦? 總不能靠猜吧?呵呵。。。 當然不能,那只能讓服務器在發送數據之前主動告訴客戶端,要發送多少數據給客戶端,然后再開始發送數據,yes, 機智如我,搞起。
先簡單測試接收數據量大小
import socket
import os,subprocess
server = socket.socket() #獲得socket實例
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("localhost",9998)) #綁定ip port
server.listen() #開始監聽
while True: #第一層loop
print("等待客戶端的連接...")
conn,addr = server.accept() #接受並建立與客戶端的連接,程序在此處開始阻塞,只到有客戶端連接進來...
print("新連接:",addr )
while True:
data = conn.recv(1024)
if not data:
print("客戶端斷開了...")
break #這里斷開就會再次回到第一次外層的loop
print("收到命令:",data)
#res = os.popen(data.decode()).read() #py3 里socket發送的只有bytes,os.popen又只能接受str,所以要decode一下
res = subprocess.Popen(data,shell=True,stdout=subprocess.PIPE).stdout.read() #跟上面那條命令的效果是一樣的
if len(res) == 0:
res = "cmd exec success,has not output!"
conn.send(str(len(res)).endcode("utf-8")) #發送數據之前,先告訴客戶端要發多少數據給它
conn.sendall(res.encode("utf-8")) #發送端也有最大數據量限制,所以這里用sendall,相當於重復循環調用conn.send,直至數據發送完畢
server.close()
import socket
client = socket.socket()
client.connect(("localhost",9998))
while True:
msg = input(">>:").strip()
if len(msg) == 0:continue
client.send( msg.encode("utf-8") )
res_return_size = client.recv(1024) #接收這條命令執行結果的大小
print("getting cmd result , ", res_return_size)
total_rece_size = int(res_return_size)
print(total_rece_size)
#print(data.decode()) #命令執行結果
client.close()
|
1
2
3
4
5
6
7
8
9
|
結果輸出:<br>
/
Library
/
Frameworks
/
Python.framework
/
Versions
/
3.5
/
bin
/
python3.
5
/
Users
/
jieli
/
PycharmProjects
/
python基礎
/
自動化day8socket
/
sock_client.py
>>:cat
/
var
/
log
/
system.log
getting cmd result , b
'3472816Sep 9 09:06:37 Jies-MacBook-Air kernel[0]: hibernate image path: /var/vm/sleepimage\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: efi pagecount 65\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: hibernate_page_list_setall(preflight 1) start\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: hibernate_page_list_setall time: 211 ms\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: pages 1211271, wire 225934, act 399265, inact 4, cleaned 0 spec 97, zf 3925, throt 0, compr 218191, xpmapped 40000\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: could discard act 94063 inact 129292 purgeable 58712 spec 81788 cleaned 0\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: WARNING: hibernate_page_list_setall skipped 47782 xpmapped pages\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: hibernate_page_list_setall preflight pageCount 225934 est comp 41 setfile 421527552 min 1073741824\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: kern_open_file_for_direct_io(0)\nSep 9 09:06:37 Jies-MacBook-Air kernel[0]: kern_open_file_for_direct_io took 181 ms\nSep 9 '
Traceback (most recent call last):
File
"/Users/jieli/PycharmProjects/python基礎/自動化day8socket/sock_client.py"
, line
17
,
in
<module>
total_rece_size
=
int
(res_return_size)
ValueError: invalid literal
for
int
() with base
10
: b'
3472816Sep
9
09
:
06
:
37
Jies
-
MacBook
-
Air kernel[
0
]: hibernate image path:
/
var
/
vm
/
sleepimage\nSep
9
09
:
06
:
37
Jies
-
MacBook
-
Air kernel[
0
]: efi pagecount
65
\nSep
9
09
:
06
:
37
Jies
-
MacBook
-
Air kernel[
0
]:
Process finished with exit code
1
|
看程序執行報錯了, 我在客戶端本想只接服務器端命令的執行結果,但實際上卻連命令結果也跟着接收了一部分。 這是為什么呢???服務器不是只send了結果的大小么?不應該只是個數字么?尼瑪命令結果不是第2次send的時候才發送的么??,擦,擦,擦,價值觀都要崩潰了啊。。。。
哈哈,這里就引入了一個重要的概念,“粘包”(測試時,Windows不會,Linux會), 即服務器端你調用時send 2次,但你send調用時,(問題八)數據其實並沒有立刻被發送給客戶端,而是放到了系統的socket發送緩沖區里,等緩沖區滿了、或者數據等待超時了,數據才會被send到客戶端,這樣就把好幾次的小數據拼成一個大數據,統一發送到客戶端了,這么做的目地是為了提高io利用效率,一次性發送總比連發好幾次效率高嘛。 但也帶來一個問題,就是“粘包”,即2次或多次的數據粘在了一起統一發送了。就是我們上面看到的情況 。
我們在這里必須要想辦法把粘包分開, 因為不分開,你就沒辦法取出來服務器端返回的命令執行結果的大小呀。so ,那怎么分開呢?首先你是沒辦法讓緩沖區強制刷新把數據發給客戶端的。 你能做的,只有一個。就是,讓緩沖區超時,超時了,系統就不會等緩沖區滿了,會直接把數據發走,因為不能一個勁的等后面的數據呀,等太久,會造成數據延遲了,那可是極不好的。so如果(改進八)讓緩沖區超時呢?
答案就是:
- time.sleep(0.5),經多次測試,讓服務器程序sleep 至少0.5就會造成緩沖區超時。哈哈哈, 你會說,擦,這么玩不會被老板開除么,雖然我們覺得0.5s不多,但是對數據實時要求高的業務場景,比如股票交易,過了0.5s 股票價格可以就漲跌很多,搞毛線呀。但沒辦法,我剛學socket的時候 找不到更好的辦法,就是這么玩的,現在想想也真是low呀
- 但現在我是有Tesla的男人了,不能再這么low了, 所以推出nb新姿勢就是, 不用sleep,服務器端每發送一個數據給客戶端,就立刻等待客戶端進行回應,即調用 conn.recv(1024), 由於recv在接收不到數據時是阻塞的,這樣就會造成,服務器端接收不到客戶端的響應,就不會執行后面的conn.sendall(命令結果)的指令,收到客戶端響應后,再發送命令結果時,緩沖區就已經被清空了,因為上一次的數據已經被強制發到客戶端了。 好機智 , 看下面代碼實現。
#_*_coding:utf-8_*_
__author__ = 'Alex Li'
import socket
import os,subprocess
server = socket.socket() #獲得socket實例
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("localhost",9999)) #綁定ip port
server.listen() #開始監聽
while True: #第一層loop
print("等待客戶端的連接...")
conn,addr = server.accept() #接受並建立與客戶端的連接,程序在此處開始阻塞,只到有客戶端連接進來...
print("新連接:",addr )
while True:
data = conn.recv(1024)
if not data:
print("客戶端斷開了...")
break #這里斷開就會再次回到第一次外層的loop
print("收到命令:",data)
#res = os.popen(data.decode()).read() #py3 里socket發送的只有bytes,os.popen又只能接受str,所以要decode一下
res = subprocess.Popen(data,shell=True,stdout=subprocess.PIPE).stdout.read() #跟上面那條命令的效果是一樣的
if len(res) == 0:
res = "cmd exec success,has not output!".encode("utf-8")
conn.send(str(len(res)).encode("utf-8")) #發送數據之前,先告訴客戶端要發多少數據給它
print("等待客戶ack應答...")
client_final_ack = conn.recv(1024) #等待客戶端響應
print("客戶應答:",client_final_ack.decode())
print(type(res))
conn.sendall(res) #發送端也有最大數據量限制,所以這里用sendall,相當於重復循環調用conn.send,直至數據發送完畢
server.close()
#_*_coding:utf-8_*_
__author__ = 'Alex Li'
import socket
import sys
client = socket.socket()
client.connect(("localhost",9999))
while True:
msg = input(">>:").strip()
if len(msg) == 0:continue
client.send( msg.encode("utf-8") )
res_return_size = client.recv(1024) #接收這條命令執行結果的大小
print("getting cmd result , ", res_return_size)
total_rece_size = int(res_return_size)
print("total size:",res_return_size)
client.send("准備好接收了,發吧loser".encode("utf-8"))
received_size = 0 #已接收到的數據
cmd_res = b''
f = open("test_copy.html","wb")#把接收到的結果存下來,一會看看收到的數據 對不對
while received_size != total_rece_size: #代表還沒收完
data = client.recv(1024)
received_size += len(data) #為什么不是直接1024,還判斷len干嘛,注意,實際收到的data有可能比1024少
cmd_res += data
else:
print("數據收完了",received_size)
#print(cmd_res.decode())
f.write(cmd_res) #把接收到的結果存下來,一會看看收到的數據 對不對
#print(data.decode()) #命令執行結果
client.close()

