python走起之第八話

1. Socket介紹

概念

network socket is an endpoint of a connection across a computer network. Today, most communication between computers is based on the Internet Protocol; therefore most network sockets are Internet sockets. More precisely, a socket is a handle (abstract reference) that a local program can pass to the networking application programming interface (API) to use the connection, for example "send this data on this socket".html

For example, to send "Hello, world!" via TCP to port 80 of the host with address 1.2.3.4, one might get a socket, connect it to the remote host, send the string, then close the socket.python

實現一個socket至少要分如下幾步,(僞代碼)linux

1
2
3
4
Socket socket  =  getSocket( type  =  "TCP" )   #設定好協議類型
connect(socket, address  =  "1.2.3.4" , port  =  "80" #鏈接遠程機器
send(socket,  "Hello, world!" #發送消息
close(socket)  #關閉鏈接

socket API is an application programming interface (API), usually provided by the operating system, that allows application programs to control and use network sockets. Internet socket APIs are usually based on the Berkeley sockets standard. In the Berkeley sockets standard, sockets are a form of file descriptor (a file handle), due to the Unix philosophy that "everything is a file", and the analogies between sockets and files: you can read, write, open, and close both.   緩存

socket address is the combination of an IP address and a port number, much like one end of a telephone connection is the combination of a phone number and a particular extension. Sockets need not have an address (for example for only sending data), but if a program binds a socket to an address, the socket can be used to receive data sent to that address. Based on this address, internet sockets deliver incoming data packets to the appropriate application process or thread.服務器

Socket Families(地址簇)

socket.AF_UNIX unix本機進程間通訊 網絡

socket.AF_INET IPV4 多線程

socket.AF_INET6  IPV6併發

These constants represent the address (and protocol) families, used for the first argument to socket(). If the AF_UNIX constant is not defined then this protocol is unsupported. More constants may be available depending on the system.app

Socket Types

socket.SOCK_STREAM  #for tcpdom

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一般僅限於高級用戶或管理員運行的程序使用。

socket.SOCK_SEQPACKET #廢棄了

These constants represent the socket types, used for the second argument to socket(). More constants may be available depending on the system. (Only SOCK_STREAM and SOCK_DGRAM appear to be generally useful.)

 

2. Socket 參數介紹

socket.socket(family=AF_INETtype=SOCK_STREAMproto=0fileno=None)  必會

Create a new socket using the given address family, socket type and protocol number. The address family should be AF_INET (the default), AF_INET6AF_UNIXAF_CAN or AF_RDS. The socket type should beSOCK_STREAM (the default), SOCK_DGRAMSOCK_RAW or perhaps one of the other SOCK_ constants. The protocol number is usually zero and may be omitted or in the case where the address family is AF_CAN the protocol should be one of CAN_RAW or CAN_BCM. If fileno is specified, the other arguments are ignored, causing the socket with the specified file descriptor to return. Unlike socket.fromfd()fileno will return the same socket and not a duplicate. This may help close a detached socket using socket.close().

socket.socketpair([family[, type[, proto]]])

Build a pair of connected socket objects using the given address family, socket type, and protocol number. Address family, socket type, and protocol number are as for the socket() function above. The default family is AF_UNIX if defined on the platform; otherwise, the default is AF_INET.

socket.create_connection(address[, timeout[, source_address]])

Connect to a TCP service listening on the Internet address (a 2-tuple (host, port)), and return the socket object. This is a higher-level function than socket.connect(): if host is a non-numeric hostname, it will try to resolve it for both AF_INET and AF_INET6, and then try to connect to all possible addresses in turn until a connection succeeds. This makes it easy to write clients that are compatible to both IPv4 and IPv6.

Passing the optional timeout parameter will set the timeout on the socket instance before attempting to connect. If no timeout is supplied, the global default timeout setting returned by getdefaulttimeout() is used.

If supplied, source_address must be a 2-tuple (host, port) for the socket to bind to as its source address before connecting. If host or port are ‘’ or 0 respectively the OS default behavior will be used.

socket.getaddrinfo(hostportfamily=0type=0proto=0flags=0) #獲取要鏈接的對端主機地址 必會

sk.bind(address) 必會

  s.bind(address) 將套接字綁定到地址。address地址的格式取決於地址族。在AF_INET下,以元組(host,port)的形式表示地址。

sk.listen(backlog) 必會

  開始監聽傳入鏈接。backlog指定在拒絕鏈接以前,能夠掛起的最大鏈接數量。

      backlog等於5,表示內核已經接到了鏈接請求,但服務器尚未調用accept進行處理的鏈接個數最大爲5
      這個值不能無限大,由於要在內核中維護鏈接隊列

sk.setblocking(bool) 必會

  是否阻塞(默認True),若是設置False,那麼accept和recv時一旦無數據,則報錯。

sk.accept() 必會

  接受鏈接並返回(conn,address),其中conn是新的套接字對象,能夠用來接收和發送數據。address是鏈接客戶端的地址。

  接收TCP 客戶的鏈接(阻塞式)等待鏈接的到來

sk.connect(address) 必會

  鏈接到address處的套接字。通常,address的格式爲元組(hostname,port),若是鏈接出錯,返回socket.error錯誤。

sk.connect_ex(address)

  同上,只不過會有返回值,鏈接成功時返回 0 ,鏈接失敗時候返回編碼,例如:10061

sk.close() 必會

  關閉套接字

sk.recv(bufsize[,flag]) 必會

  接受套接字的數據。數據以字符串形式返回,bufsize指定最多能夠接收的數量。flag提供有關消息的其餘信息,一般能夠忽略。

sk.recvfrom(bufsize[.flag])

  與recv()相似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。

sk.send(string[,flag]) 必會

  將string中的數據發送到鏈接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。即:可能未將指定內容所有發送。

sk.sendall(string[,flag]) 必會

  將string中的數據發送到鏈接的套接字,但在返回以前會嘗試發送全部數據。成功返回None,失敗則拋出異常。

      內部經過遞歸調用send,將全部內容發送出去。

sk.sendto(string[,flag],address)

  將數據發送到套接字,address是形式爲(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。該函數主要用於UDP協議。

sk.settimeout(timeout) 必會

  設置套接字操做的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。通常,超時期應該在剛建立套接字時設置,由於它們可能用於鏈接的操做(如 client 鏈接最多等待5s )

sk.getpeername()  必會

  返回鏈接套接字的遠程地址。返回值一般是元組(ipaddr,port)。

sk.getsockname() 

  返回套接字本身的地址。一般是一個元組(ipaddr,port)

sk.fileno() 

  套接字的文件描述符

socket.sendfile(fileoffset=0count=None)

     發送文件 ,但目前多數狀況下並沒有什麼卵用。

 

3. 基本Socket實例

前面講了這麼多,到底咋麼用呢?

  SocketServer.py
  SocketClient.py

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

 

屢次的數據交互怎麼實現呢?

  socketserver 支持屢次交互
  socket客戶端支持多交互

實現了屢次交互, 棒棒的, 但你會發現一個小問題, 就是客戶端一斷開,服務器端就進入了死循環,爲啥呢?

看客戶端斷開時服務器端的輸出

1
2
3
4
5
6
7
8
9
等待客戶端的鏈接...
新鏈接: ( '127.0.0.1' 62722 )
收到消息: b 'hey'
收到消息: b 'you'
收到消息: b''   #客<span style="color: #ff0000;">戶端一斷開,服務器端就收不到數據了,可是不會報錯,就進入了死循環模式。。</span>。
收到消息: b''
收到消息: b''
收到消息: b''
收到消息: b''

知道了緣由就好解決了,只須要加個判斷服務器接到的數據是否爲空就行了,爲空就表明斷了。。。

  加了判斷客戶端是否斷開的代碼

 

4.Socket實現多鏈接處理

上面的代碼雖然實現了服務端與客戶端的屢次交互,可是你會發現,若是客戶端斷開了, 服務器端也會跟着馬上斷開,由於服務器只有一個while 循環,客戶端一斷開,服務端收不到數據 ,就會直接break跳出循環,而後程序就退出了,這顯然不是咱們想要的結果 ,咱們想要的是,客戶端若是斷開了,咱們這個服務端還能夠爲下一個客戶端服務,它不能斷,她接完一個客,擦完嘴角的遺留物,就要接下來勇敢的去接待下一個客人。 在這裏如何實現呢?

1
conn,addr  =  server.accept()  #接受並創建與客戶端的鏈接,程序在此處開始阻塞,只到有客戶端鏈接進來...

咱們知道上面這句話負責等待並接收新鏈接,對於上面那個程序,其實在while break以後,只要讓程序再次回到上面這句代碼這,就可讓服務端繼續接下一個客戶啦。 

注意了, 此時服務器端依然只能同時爲一個客戶服務,其客戶來了,得排隊(鏈接掛起),不能玩 three some. 這時你說想,我就想玩3p,就想就想嘛,其實也能夠,多交錢嘛,繼續往下看,後面開啓新姿式後就能夠玩啦。。。

 

5.經過socket實現簡單的ssh

光只是簡單的發消息、收消息沒意思,乾點正事,能夠作一個極簡版的ssh,就是客戶端鏈接上服務器後,讓服務器執行命令,並返回結果給客戶端。

  socket ssh server
  socket ssh client

very cool , 這樣咱們就作了一個簡單的ssh , 但多試幾條命令你就會發現,上面的程序有如下2個問題。 

  1. 不能執行top等相似的 會持續輸出的命令,這是由於,服務器端在收到客戶端指令後,會一次性經過os.popen執行,並獲得結果後返回給客戶,但top這樣的命令用os.popen執行你會發現永遠都不會結束,因此客戶端也永遠拿不到返回。(真正的ssh是經過select 異步等模塊實現的,咱們之後會涉及)
  2. 不能執行像cd這種沒有返回的指令, 由於客戶端每發送一條指令,就會經過client.recv(1024)等待接收服務器端的返回結果,可是cd命令沒有結果 ,服務器端調用conn.send(data)時是不會發送數據給客戶端的。 因此客戶端就會一直等着,等到天荒地老,結果就卡死了。解決的辦法是,在服務器端判斷命令的執行返回結果的長度,若是結果爲空,就本身加個結果返回給客戶端,如寫上"cmd exec success, has no output."
  3. 若是執行的命令返回結果的數據量比較大,會發現,結果返回不全,在客戶端上再執行一條命令,結果返回的仍是上一條命令的後半段的執行結果,這是爲何呢?這是由於,咱們的客戶寫client.recv(1024), 即客戶端一次最多隻接收1024個字節,若是服務器端返回的數據是2000字節,那有至少9百多字節是客戶端第一次接收不了的,那怎麼辦呢,服務器端此時不能把數據直接扔了呀,so它會暫時存在服務器的io發送緩衝區裏,等客戶端下次再接收數據的時候再發送給客戶端。 這就是爲何客戶端執行第2條命令時,卻接收到了第一條命令的結果的緣由。 這時有同窗說了, 那我直接在客戶端把client.recv(1024)改大一點不就行了麼, 改爲一次接收個100mb,哈哈,這是不行的,由於socket每次接收和發送都有最大數據量限制的,畢竟網絡帶寬也是有限的呀,不能一次發太多,發送的數據最大量的限制 就是緩衝區能緩存的數據的最大量,這個緩衝區的最大值在不一樣的系統上是不同的, 我實在查不到一個具體的數字,但測試的結果是,在linux上最大一次可接收10mb左右的數據,不過官方的建議是不超過8k,也就是8192,而且數據要能夠被2整除,不要問爲何 。anyway , 若是一次只能接收最多不超過8192的數據 ,那服務端返回的數據超過了這個數字怎麼辦呢?好比讓服務器端打開一個5mb的文件並返回,客戶端怎麼才能完整的接受到呢?那就只能循環收取啦。 

 

在開始解決上面問題3以前,咱們要考慮,客戶端要循環接收服務器端的大量數據返回直到一條命令的結果所有返回爲止, 但問題是客戶端知道服務器端返回的數據有多大麼?答案是不知道,那既然不知道服務器的要返回多大的數據,那客戶端怎麼知道要循環接收多少次呢?答案是不知道,擦,那咋辦? 總不能靠猜吧?呵呵。。。 固然不能,那隻能讓服務器在發送數據以前主動告訴客戶端,要發送多少數據給客戶端,而後再開始發送數據,yes, 機智如我,搞起。

先簡單測試接收數據量大小

  ssh server 返回執行結果大小
  ssh client 接收執行結果的大小
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的時候才發送的麼??,擦,擦,擦,價值觀都要崩潰了啊。。。。

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

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

答案就是:

  1. time.sleep(0.5),經屢次測試,讓服務器程序sleep 至少0.5就會形成緩衝區超時。哈哈哈, 你會說,擦,這麼玩不會被老闆開除麼,雖然咱們以爲0.5s很少,可是對數據實時要求高的業務場景,好比股票交易,過了0.5s 股票價格能夠就漲跌不少,搞毛線呀。但沒辦法,我剛學socket的時候 找不到更好的辦法,就是這麼玩的,如今想一想也真是low呀
  2. 但如今我是有Tesla的男人了,不能再這麼low了, 因此推出nb新姿式就是, 不用sleep,服務器端每發送一個數據給客戶端,就馬上等待客戶端進行迴應,即調用 conn.recv(1024), 因爲recv在接收不到數據時是阻塞的,這樣就會形成,服務器端接收不到客戶端的響應,就不會執行後面的conn.sendall(命令結果)的指令,收到客戶端響應後,再發送命令結果時,緩衝區就已經被清空了,由於上一次的數據已經被強制發到客戶端了。 好機智 , 看下面代碼實現。
  接收大數據 server端
  接收大數據客戶端

 

 

6. SocketServer

The socketserver module simplifies the task of writing network servers.

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 )

These more infrequently used classes are similar to the TCP and UDP classes, but use Unix domain sockets; they’re not available on non-Unix platforms. The parameters are the same as for TCPServer.

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 BaseRequestHandlerclass 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() orserve_forever() method of the server object to process one or many requests.
  4. Finally, call server_close() to close the socket.

基本的socketserver代碼

  View Code

但你發現,上面的代碼,依然不能同時處理多個鏈接,擦,那我搞這個幹嗎?別急,不是不能處理多併發,若是你想,你還要啓用多線程,多線程咱們如今還沒講,但你大致知道,有了多線程,就能同時讓cpu幹多件事了就行先。

 

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

class socketserver.ForkingTCPServer

class socketserver.ForkingUDPServer

class socketserver.ThreadingTCPServer

class socketserver.ThreadingUDPServer

 

so 只須要把下面這句

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

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

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

 

class socketserver.BaseServer(server_addressRequestHandlerClass) 主要有如下方法

  View Code
相關文章
相關標籤/搜索