Socket



1.不同的協議在同一條網線上傳遞(傳遞的都是數據流)
1.send 發
2.receive 收
2.OSI七層協議
應用層
表示層
會話層
傳輸層
網絡層 ip
數據鏈路層 mac
物理層
傳輸層:數據通信需要規則:
TCP/IP : 三次握手 、四次斷開 syn syn+ack ack
UDP
3. 一台電腦上可開辟的端口數為:65535 port
nginx:80 mysql:3306
4.模擬通信
發送端:
import socket : 因為socket封裝了所有的上層協議,所以在使用時,需要指明要使用的協議
socket.TCP/IP (聲明協議類型)
connect(a.ip, a.port) 建立鏈接,鏈接對方的ip以及端口號
socket.send(hello) 發送數據
rece() 接收數據
socket.close() 關閉鏈接
接收端: 注意,先要有接收端(服務端),再有發送端
import socket
socket.TCP/IP (聲明協議類型)
listen(0.0.0.0, 3306) 監聽(術語):因為一台機器有多個網卡,所以要先聲明自己的網卡(0.0.0.0表示選擇所有網卡),再聲明自己的端口號
waiting() 等待接收數據
reve() 接收數據
send() 發送數據
服務器端不需要關閉,等待其他程序來連

簡單的服務器-客戶端

# -*- coding:utf8 -*-
# 客戶端
import socket
client = socket.socket()    # 聲明socket協議類型,同時生成socket鏈接對象
client.connect(("localhost", 6996))     # 連接服務器的ip和端口號
client.send("hello你好".encode(encoding="utf8"))   # 在python3中,發送的數據都是二進制數據流,所以需要將字符串轉化為二進制
data = client.recv(1024)    # 接收服務器端發送過來的數據,大小上線為1024字節
print(data.decode())
client.close()
client
# -*- coding:utf8 -*-
# 服務器端
import socket
server = socket.socket()    # 聲明socket協議類型,同時生成socket鏈接對象
server.bind(("localhost", 6996))    # 綁定需要監聽的 網卡ip 和 端口號
server.listen()     # 監聽
print("我要開始等電話了")
"""
等待電話打進來
conn:就是客戶端連接進來,服務器端為其生成的一個鏈接實例
addr:客戶端ip
"""
conn, addr = server.accept()
print(conn, addr)
print("電話來了")
data = conn.recv(1024)  # 1024為接收客戶端發送過來數據的上限為1024字節
print("reve", data.decode())
conn.send(data.upper())  # 發送數據給客戶端
server.close()  # 關閉服務器端
server

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

 

優化后的服務器-客戶端

 光只是簡單的發消息、收消息沒意思,干點正事,可以做一個極簡版的ssh,就是客戶端連接上服務器后,讓服務器執行命令,並返回結果給客戶端。

import os
import socket
server = socket.socket()
server.bind(("localhost", 6996))
server.listen(5)  # 設置客戶端連接上限
while True:
    print("等待連接")
    conn, addr = server.accept()
    while True:
        data_1 = conn.recv(1024)
        if not data_1:
            print("客戶端斷開了...", conn.getpeername())
            break  # 這里斷開就會再次回到第一次外層的loop
        data_2 = os.popen(data_1.decode()).read()
        conn.send(str((len(data_2.encode()))).encode()) # 提醒客戶端將會發送數據的長度
        conn.send(data_2.encode())
        print("已將消息發送給:", conn.getpeername())
server.close()
服務器端
import socket
client = socket.socket()
client.connect(("localhost", 6996))
while True:
    str_input = input("請輸入命令").strip()
    if len(str_input) == 0: continue
    client.send(str_input.encode())
    data_len = client.recv(1024)
    pd_len = 0
    data_1 = b""
    while pd_len < int(data_len):
        data = client.recv(1024)
        data_1 += data
        pd_len += len(data)
    print(pd_len, data_len)
    print(data_1.decode())

client.close()
客戶端

粘包:

  • 如果出現粘包問題(通知客戶端長度的數據和需要發送給客戶端的數據,被緩沖池統一處理發送給了客戶端),只需要在發送這兩個數據中間,在進行一次交互就可以了(例如:客戶端:發送我准備好接收數據了,服務器端:接收到反饋后,給客戶端發送數據)
  • 如何解決粘包的問題?
    > 每次發送的消息時,都將消息划分為 頭部(固定字節長度) 和 數據 兩部分。例如:頭部,用4個字節表示后面數據的長度。
    > - 發送數據,先發送數據的長度,再發送數據(或拼接起來再發送)。
    > - 接收數據,先讀4個字節就可以知道自己這個數據包中的數據長度,再根據長度讀取到數據。
    • """>對於頭部需要一個數字並固定為4個字節,這個功能可以借助python的struct包來實現:"""
      import struct
      len_v1 = struct.pack("i", 999)      # i代表以int類型以4個字節 將999打包成一個4個字節的字節流
      print(len_v1)   # b'\xe7\x03\x00\x00'
      
      num_len = struct.unpack("i", len_v1)    # 以int類型的4個字節,將len_v1的字節流 轉換成一個元組
      print(num_len)  # (999,)
    •  

       示例代碼:

      """解決粘包問題的服務器端"""
      import socket
      import struct
      
      # 聲明協議並創建鏈接對象
      server = socket.socket()
      # server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 設置ip可復用,參數暫時沒有掌握
      # 聲明ip和端口號
      server.bind(("localhost", 5002))
      # 設置監聽為5
      server.listen(5)
      # 等待連接
      conn, addr = server.accept()
      # 接收第一條數據,先接收數據的長度
      data_len = conn.recv(4)
      leng = struct.unpack('i', data_len)[0]
      recv_len = 0  # 記錄已經接收數據的長度
      data_recv_sum = b""  # 記錄接收的所有數據
      while True:
          length = leng - recv_len  # 剩余數據長度
          length = length if length < 1024 else 1024
          data_recv = conn.recv(length)
          data_recv_sum += data_recv
          recv_len += len(data_recv)
          if recv_len == leng:
              break
      print(data_recv_sum.decode("utf_8"))        # 9527:你好中國
      
      # 接收第二條數據同理
      data_len = conn.recv(4)
      leng = struct.unpack("i", data_len)[0]
      recv_len = 0  # 記錄已經接收數據的長度
      data_recv_sum = b""  # 記錄接收的所有數據
      while True:
          length = leng - recv_len  # 剩余數據長度
          length = length if length < 1024 else 1024
          data_recv = conn.recv(length)
          data_recv_sum += data_recv
          recv_len += len(data_recv)
          if recv_len == leng:
              break
      print(data_recv_sum.decode("utf_8"))        # 9527:你好中國,哈哈哈
      服務器端
      """解決粘包問題的服務器端"""
      import socket
      import struct
      # 聲明協議並創建鏈接對象
      client = socket.socket()
      # 創建連接
      client.connect(("localhost", 5002))
      # 發送第一條數據
      data_send = "9527:你好中國".encode("utf_8")
      data_len = struct.pack("i", len(data_send))
      client.sendall(data_len)
      client.sendall(data_send)
      
      # 發送第二條數據
      data_send = "9527:你好中國,哈哈哈".encode("utf_8")
      data_len = struct.pack("i", len(data_send))
      client.sendall(data_len)
      client.sendall(data_send)
      客戶端

Socket中的方法:

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)

SocketServer實現並發:

第一步: 你必須自己創建一個請求處理類,並且這個類要進程BaseRequestHandler,並且還要重寫父類中的Handler()方法
第二步: 你必須實例化TCPServer,並且傳遞server ip 和 你上面創建的請求處理類 給這個TCPServer
第三步: server.randler_reques()只處理一個請求(所以一般不用)
server.serve_forever()處理多個一個請求,永遠執行
第四步: 關閉服務器 server.close()
# -*- coding:UTF-8 -*-
import socketserver
# 創建請求類
class MySocketServer(socketserver.BaseRequestHandler):
    # 與客戶端所有的交互都重寫在這個方法中
    def handle(self) :
        while True:
            try:
                # 在Python3中客戶端斷開鏈接,服務器端會拋出異常
                self.data = self.request.recv(1024).strip()
                print("客戶端地址為:{}".format(self.client_address[0]))
                print(self.data.decode())
                self.request.send(self.data.upper())
            except ConnectionResetError as reason:
                print(reason)
                break

if __name__ == "__main__":
    HOST, PORK = "localhost", 999
    # 這句代碼可以實現服務器與客戶端的一對一,但是不能一對多
    #server = socketserver.TCPServer((HOST, PORK), MySocketServer)
    # 把代碼改為下面的代碼,則可以實現,一對多
    server = socketserver.ThreadingTCPServer((HOST, PORK), MySocketServer)
    server.serve_forever()
    server.server_close()
多並發服務器端
# # -*- coding:utf8 -*-
# 客戶端
import socket
client = socket.socket()    # 聲明socket協議類型,同時生成socket鏈接對象
client.connect(("localhost", 999))     # 連接服務器的ip和端口號
while True:
    a = input("請輸入")
    client.send(a.encode(encoding="utf8"))   # 在python3中,發送的數據都是二進制數據流,所以需要將字符串轉化為二進制
    data = client.recv(1024)    # 接收服務器端發送過來的數據,大小上線為1024字節
    print(data.decode())
client.close()
客戶端

阻塞和非阻塞

默認情況下我們編寫的網絡編程的代碼都是阻塞的(等待)

# ################### socket服務端(接收者)###################
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.setblocking(False) # 加上就變為了非阻塞

sock.bind(('127.0.0.1', 8001))
sock.listen(5)

# 非阻塞
conn, addr = sock.accept()

# 非阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()

# ################### socket客戶端(發送者) ###################
import socket

client = socket.socket()

client.setblocking(False) # 加上就變為了非阻塞

# 非阻塞
client.connect(('127.0.0.1', 8001))

client.sendall('哈哈'.encode('utf-8'))

client.close()

如果代碼變成了非阻塞,程序運行時一旦遇到 acceptrecvconnect 就會拋出 BlockingIOError 的異常。

這不是代碼編寫的有錯誤,而是原來的IO阻塞變為非阻塞之后,由於沒有接收到相關的IO請求拋出的固定錯誤。

非阻塞的代碼一般與IO多路復用結合,可以迸發出更大的作用。

 

IO多路復用

  • I/O多路復用指:通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。
  • 基於 IO多路復用 + 非阻塞的特性,無論編寫socket的服務端和客戶端都可以提升性能。其中
    • IO多路復用,監測socket對象是否有變化(是否連接成功?是否有數據到來等)。

    • 非阻塞,socket的connect、recv過程不再等待。

注意:IO多路復用只能用來監聽 IO對象 是否發生變化,常見的有:文件是否可讀寫、電腦終端設備輸入和輸出、網絡請求(常見)。

在Linux操作系統化中 IO多路復用 有三種模式,分別是:select,poll,epoll。(windows 只支持select模式)

監測socket對象是否新連接到來 or 新數據到來。

select
 
select最早於1983年出現在4.2BSD中,它通過一個select()系統調用來監視多個文件描述符的數組,當select()返回后,該數組中就緒的文件描述符便會被內核修改標志位,使得進程可以獲得這些文件描述符從而進行后續的讀寫操作。
select目前幾乎在所有的平台上支持,其良好跨平台支持也是它的一個優點,事實上從現在看來,這也是它所剩不多的優點之一。
select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般為1024,不過可以通過修改宏定義甚至重新編譯內核的方式提升這一限制。
另外,select()所維護的存儲大量文件描述符的數據結構,隨着文件描述符數量的增大,其復制的開銷也線性增長。同時,由於網絡響應時間的延遲使得大量TCP連接處於非活躍狀態,但調用select()會對所有socket進行一次線性掃描,所以這也浪費了一定的開銷。
 
poll
 
poll在1986年誕生於System V Release 3,它和select在本質上沒有多大差別,但是poll沒有最大文件描述符數量的限制。
poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體復制於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增加而線性增大。
另外,select()和poll()將就緒的文件描述符告訴進程后,如果進程沒有對其進行IO操作,那么下次調用select()和poll()的時候將再次報告這些文件描述符,所以它們一般不會丟失就緒的消息,這種方式稱為水平觸發(Level Triggered)。
 
epoll
 
直到Linux2.6才出現了由內核直接支持的實現方法,那就是epoll,它幾乎具備了之前所說的一切優點,被公認為Linux2.6下性能最好的多路I/O就緒通知方法。
epoll可以同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變為就緒狀態,它只說一遍,如果我們沒有采取行動,那么它將不會再次告知,這種方式稱為邊緣觸發),理論上邊緣觸發的性能要更高一些,但是代碼實現相當復雜。
epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這里也使用了內存映射(mmap)技術,這樣便徹底省掉了這些文件描述符在系統調用時復制的開銷。
另一個本質的改進在於epoll采用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法后,內核才對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來注冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會采用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。
"""非阻塞+IO多路復用"""
"""
非阻塞:代碼不會自動檢測recv  accept connect是否有接收到訊息
IO多路復用:可以檢測accept connect recv是否有接收到訊息
    ->  IO多路復用 + 非阻塞 + socket服務端,可以讓服務端同時處理多個客戶端的請求。
    ->  IO多路復用 + 非阻塞 + socket客戶端,可以向服務端同時發起多個請求。
在Linux操作系統化中 IO多路復用 有三種模式,分別是:select,poll,epoll。(windows 只支持select模式)
    -> select:1.對socket對象列表有限制,最大為1024  2.檢測對象是否發生變化是通過遍歷的方式,所以開銷很大
    -> poll:poll和select在本質上沒有多大差別,但是poll沒有最大文件描述符數量的限制
    -> epoll:發生了變化的socket對象會告知epoll,讓其處理,但是代碼實現相當復雜
優點:1.可以在交互后做一些其他的事情 2.讓服務器支持多個客戶端連接
"""
"""IO多路復用的服務器端"""
import select
import socket
server = socket.socket()
server.bind(("localhost", 5001))
server.setblocking(False)   # 設置為非阻塞
server.listen(5)
inputs = [server, ]     # socket對象列表 -> [server, 第一個客戶端連接conn ]
while True:
    """
    r, w, e = select.select(inputs, [], [server,conn1,conn2], 0.05)
    0.05:表示,最多花費0.05秒的時間,去檢測inputs列表中的每一個socket對象是否有人向它們發起連接或數據,若沒有接收到訊息,那么([], [], [])
    返回值:1個為元組,元組中有3個列表([], [], [])
    列表r:里面存儲的是檢測到,inputs中發生了變化的socket對象(server、第一個conn、第二個conn······)
    列表w: 看客戶端(連接成功了的對象)
    列表e: 捕獲第三個列表中socket對象發生的異常,如果有某個對象發生了異常,那么會將這個對象添加到列表e中
    """
    r, w, e = select.select(inputs, [], [], 0.05)
    for sock in r:
        if sock == server:  # 表示server對象接收到了新的連接
            # 注意:因為server是有收到訊息的(因為是select處理的),所以這里沒有報錯(BlockingIOError)
            conn, addr = sock.accept()    # 把新連接的對象和ip記錄下來記錄下來
            print("新連接產生,對方ip:{}".format(addr))
            inputs.append(conn)     # 將新連接對象添加到socket對象列表中
        else:               # 否則接收到的訊息就不是新連接,而是已經建立過的連接,發送了新的訊息過來
            # 注意:因為coon(sock)是有收到訊息的(因為是select處理的),所以這里沒有報錯(BlockingIOError)
            data_recv = sock.recv(1024)
            if data_recv:
                print(data_recv.decode("utf-8"))
            else:
                print("ip:{}斷開連接".format(sock.getpeername()[1]))
                inputs.remove(sock)
    # 可以在這里做一些其他的事情
# ################### socket客戶端 ###################
import socket

client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))

while True:
    content = input(">>>")
    if content.upper() == 'Q':
        break
    client.sendall(content.encode('utf-8'))

client.close()
"""IO多路復用的客戶端"""
# 優點:偽造並發現象
import select
import socket
client_list = []    # 存儲client連接對象的列表
for x in range(5):
    client = socket.socket()
    client.setblocking(False)   # 設置為非阻塞
    try:
        client.connect(("localhost", 5001))     # 雖然連接建立了,但是依然會拋出異常
    except BlockingIOError as e:
        pass
    client_list.append(client)      # 將連接對象添加到列表中

recv_list = []  # 存放連接成功了的client對象,用於檢測服務器端是否有給客戶端發送訊息
while True:
    """
    r, w, e =select.select([], client_list, [], 0.1)
    列表w:client_list列表中與客戶端連接成功了的client對象
    """
    r, w, e = select.select([], client_list, [], 0.1)
    for sock in w:
        # 向服務器發送請求,如發送下載圖片的請求
        sock.sendall(b"GET /nginx-logo.png HTTP/1.1\r\nHost:47.98.134.86\r\n\r\n")
        # 存放連接成功了的client對象,用於檢測服務器端是否有給客戶端發送訊息
        recv_list.append(sock)
        # 移除存放在client_list中的client對象,否則,會一直執行這個循環,導致客戶端重復向服務器發送下載圖片的請求
        client_list.remove(sock)
    for sock in r:
        # 數據發送成功后,接收的返回值(圖片)並寫入到本地文件中(省略代碼)
        data = sock.recv(8196)
        # 根據需求是否移除recv_list列表中client對象
        recv_list.remove(sock)
        pass
    # 判斷client是否都處理完成
    if not recv_list and not client_list:
        break

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM