網路編程之Socket程式碼實例

  • 2019 年 10 月 3 日
  • 筆記

網路編程之Socket程式碼實例

一、基本Socket例子

Server端:

# Echo server program  import socket    HOST = ''                 # Symbolic name meaning all available interfaces  PORT = 50007              # Arbitrary non-privileged port    sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  sock_server.bind((HOST, PORT))    sock_server.listen(1) #開始監聽,1代表在允許有一個連接排隊,更多的新連接連進來時就會被拒絕  conn, addr = sock_server.accept() #阻塞直到有連接為止,有了一個新連接進來後,就會為這個請求生成一個連接對象    with conn:      print('Connected by', addr)      while True:          data = conn.recv(1024) #接收1024個位元組          if not data: break #收不到數據,就break          conn.sendall(data) #把收到的數據再全部返回給客戶端

Client端:

# Echo client program  import socket    HOST = 'localhost'    # The remote host  PORT = 50007              # The same port as used by the server    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  client.connect((HOST, PORT))  client.sendall(b'Hello, world')    data = client.recv(1024)    print('Received',data)

先啟動Server端,再啟動Client端,結果如下:

二、循環收發數據

第一次接觸就這麼交待了,之說了一句話,感覺不夠過癮,如何實現更多的交互呢?簡單,只需要讓客戶端不斷的發,服務端不斷的收就可以了,寫個循環搞定。

Server端:

# Echo server program  import socket    HOST = ''                 # Symbolic name meaning all available interfaces  PORT = 50007              # Arbitrary non-privileged port    sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  sock_server.bind((HOST, PORT))    sock_server.listen(1) #開始監聽,1代表在允許有一個連接排隊,更多的新連接連進來時就會被拒絕  conn, addr = sock_server.accept() #阻塞直到有連接為止,有了一個新連接進來後,就會為這個請求生成一個連接對象    with conn:      print('Connected by', addr)      while True:          data = conn.recv(1024) #接收1024個位元組          print("server recv:",conn.getpeername(), data.decode())          if not data: break #收不到數據,就break          conn.sendall(data) #把收到的數據再全部返回給客戶端

Client端:

# Echo client program  import socket    HOST = 'localhost'    # The remote host  PORT = 50007              # The same port as used by the server    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  client.connect((HOST, PORT))    while True:        msg = input(">>>:").strip()      if len(msg) == 0:continue        client.sendall(msg.encode()) #發送用戶輸入的數據,必須是bytes模式        data = client.recv(1024)        print('Received',data.decode()) #收到伺服器的響應後,decode一下

三、簡單聊天軟體

上面的例子,服務端只是將客戶端發來的再發送給客戶端,這哪叫聊天啊,這種事需要雙方配合,得讓服務端也能說話。

Server端:

import socket    HOST = ''                 # Symbolic name meaning all available interfaces  PORT = 50007              # Arbitrary non-privileged port    sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  sock_server.bind((HOST, PORT))    sock_server.listen(1) #開始監聽,1代表在允許有一個連接排隊,更多的新連接連進來時就會被拒絕  conn, addr = sock_server.accept() #阻塞直到有連接為止,有了一個新連接進來後,就會為這個請求生成一個連接對象    with conn:      print('Connected by', addr)      while True:          data = conn.recv(1024) #接收1024個位元組          print("recv from Alex:",conn.getpeername(), data.decode())          if not data: break #收不到數據,就break            response = input(">>>").strip()          conn.send(response.encode())          print("send to alex:",response)

Client不需要做更改,直接看結果:

以上的例子還是有bug,雙方只能一來一往的說話,如果你想來納許發2句話是不行的,會卡住。這是因為你發了一條消息後,就去調用recv方法接收伺服器的響應了,再伺服器端返回消息之前,這個recv(1024)方法是阻塞的,如果想允許此時還能再發消息給伺服器端,就需要再單獨啟動一個執行緒,只負責發消息。

四、聊天軟體升級版

剛才在聊天的時候,服務端在服務客戶端的時候,其它人如果也想跟服務端連接是處於排隊狀態,然後等正在被服務的客戶端完事並斷開後,下一個人就跟上,但實際情況是客戶端一斷開,服務端也跟著斷了。

為什麼會斷呢?引文服務端以下程式碼的意思是,如果收不到數據,就跳出循環,就斷開了。

conn, addr = sock_server.accept() #阻塞直到有連接為止,有了一個新連接進來後,就會為這個請求生成一個連接對象    with conn:      print('Connected by', addr)      while True:          data = conn.recv(1024) #接收1024個位元組          print("recv from Alex:",conn.getpeername(), data.decode())            if not data: break #收不到數據,就break , 就是它乾的            response = input(">>>").strip()          conn.send(response.encode())          print("send to alex:",response)

想實現一個客戶端斷開後,可以立刻接入另外一個客戶端的話,怎麼辦呢?只需要再在外層加個循環。

while True: #最外層loop        conn, addr = sock_server.accept() #阻塞直到有連接為止,有了一個新連接進來後,就會為這個請求生成一個連接對象      #為何把上面這句話也包含在循環里?      print("來了個新客人",conn.getpeername() )        with conn:          print('Connected by', addr)          while True:              data = conn.recv(1024) #接收1024個位元組              print("recv from :",conn.getpeername(), data.decode())              if not data: break #收不到數據,就break              conn.send(data.upper())              print("send to alex:",data)

break 跳出後就回到大while那層:

但是,有的人在重啟服務端時可能會遇到:

這是由於你的服務端仍然存在4次揮手的time_wait狀態,在佔用地址(如果不懂,請深入研究:1、tcp三次握手,四次揮手。2、sun洪水攻擊。3、伺服器高並發情況下會有大量的time_wait狀態的優化方法)

解決方法1:

sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  sock_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #一行程式碼搞定,寫在bind之前  sock_server.bind((HOST, PORT))

解決方法2(用於Linux系統):

發現系統存在大量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 時間

五、UDP實例

UDP不需要經過3次握手和4次揮手,不需要提前建立連接,直接發數據就行。

Server端:

import socket  ip_port=('127.0.0.1',9000)  BUFSIZE=1024  udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #udp類型    udp_server_client.bind(ip_port)    while True:      msg,addr=udp_server_client.recvfrom(BUFSIZE)      print("recv ",msg,addr)        udp_server_client.sendto(msg.upper(),addr)

Client端:

import socket  ip_port = ('127.0.0.1',9000)  BUFSIZE = 1024  udp_server_client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)    while True:      msg=input('>>: ').strip()      if not msg:continue      udp_server_client.sendto(msg.encode('utf-8'),ip_port)        back_msg,addr = udp_server_client.recvfrom(BUFSIZE)      print(back_msg.decode('utf-8'),addr)

結果:

六、TCP  VS  UDP

1、TCP基於鏈接通訊

  • 基於鏈接,則需要listen(backlog),指定連接池的大小。
  • 基於鏈接,必須先運行服務端,然後再由客戶端發起鏈接請求。
  • 對於mac系統:如果一端斷開了鏈接,那另外一端的鏈接也跟著完蛋,recv將不會阻塞,接收到的是空(解決方法:服務端通訊循環內加異常處理,捕捉到異常後就break通訊循環)
  • 對於windows/Linux系統:如果一端斷開了鏈接,那另外一端的鏈接也跟著完蛋,recv將不會阻塞,收到的是空(解決方法:服務端通訊循環內加異常處理,捕捉到異常後就break通訊循環)

2、UDP無鏈接

  • 無鏈接,因而無需listen(backlog),更加沒有什麼連接池之說了。
  • 無鏈接,UDP的sendinto不用管是否有一個正在運行的服務端,可以己端一個勁地發消息,只不過數據會丟失。
  • recvfrom收的數據小於sendinto發送的數據時,在mac和Linux系統上數據直接丟失,在windows系統則會直接報錯。
  • 只有sendinto發送數據沒有recvfrom收數據,則數據丟失。