1. 程式人生 > >Python進階之網路程式設計

Python進階之網路程式設計

網路通訊

使用網路的目的

把多方連結在一起,進行資料傳遞;
網路程式設計就是,讓不同電腦上的軟體進行資料傳遞,即程序間通訊;

ip地址

ip地址概念和作用

IP地址是什麼:比如192.168.1.1 這樣的一些數字;
ip地址的作用:用來在電腦中 標識唯一一臺電腦,比如192.168.1.1;在本地區域網是唯一的。

網絡卡資訊

檢視網絡卡資訊

Linux:ifconfig
windows:ipconfig

  • ensxx:用來與外部進行通訊的網絡卡;
  • lo:環回網絡卡,用來進行本地通訊的;

linux關閉/開啟網絡卡:sudo ifconfig ensxx down/up

ip和ip地址的分類

ip分為ipv4和ipv6

ip地址分為:

  • A類地址
  • B類地址
  • C類地址
  • D類地址--用於多播
  • E類地址--保留地址,因ipv6誕生,已無用
  • 私有ip

單播--一對一
多播--一對多
廣播--多對多

ip:標識電腦;
埠:標識電腦上的程序(正在執行的程式);
ip和埠一起使用,唯一標識主機中的應用程式,進行統一軟體的通訊;

埠分類

知名埠

固定分配給特定程序的埠號,其他程序一般無法使用這個埠號;
小於1024的,大部分都是知名埠;
範圍從0~1023;

動態埠

不固定分配,動態分配,使用後釋放的埠號;
範圍1024~65535;

socket

socket的概念

socket是程序間通訊的一種方式,能實現不同主機間的程序間通訊,即socket是用來網路通訊必備的東西;

建立socket

建立套接字:

import socket
soc = socket.socket(AddressFamily, Type)

函式socket.socket建立一個socket,該函式有兩個引數:
Address Family:可選 AF_INET(用於internet程序間通訊)和AF_UNIX(用於同一臺機器程序間通訊);
Type:套接字型別,可選 SOCK_STREAM(流式套接字,主用於TCP協議)/SOCK_DGRAM(資料報套接字,主用於UDP套接字);

建立tcp套接字

import socket

soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
...
soc.close()

建立udp套接字

import socket

soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
...
soc.close()

udp

udp使用socket傳送資料

在同一區域網內發訊息;
如果用虛擬機器和windows,要用橋接模式,確保在同一區域網內;

import socket


def main():
    # 建立一個udp套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 使用套接字收發資料
    udp_socket.sendto(b"hahaha", ("193.168.77.1", 8080))
    # 關閉套接字
    udp_socket.close()


if __name__ == "__main__":
    main()

udp傳送資料的幾種情況:

  1. 在固定資料的引號前加b,不能使用於使用者自定義資料;
  2. 使用者自定義資料,並進行傳送,使用.encode("utf-8")進行encode編碼
  3. 使用者迴圈傳送資料
  4. 使用者迴圈傳送資料並可以退出

只貼出最後一種情況,即完整程式碼

import socket


def main():
    # 建立一個udp套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while 1:
        # 從鍵盤獲取要傳送的資料
        send_data = input("請輸入你要傳送的資料:")
        if send_data == "exit":
            break
        # 使用套接字收發資料
        udp_socket.sendto(send_data.encode("utf-8"), ("193.168.77.1", 8080))

    # 關閉套接字
    udp_socket.close()


if __name__ == "__main__":
    main()

udp接收資料

接收到的資料是一個元組,元組第一部分是傳送方傳送的內容,元組第二部分是傳送方的ip地址和埠號;

import socket


def main():
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    localaddr = ('', 8080)
    udp_socket.bind(localaddr)  # 必須繫結自己電腦的ip和埠

    # 接收資料
    recv_data = udp_socket.recvfrom(1024)
    # recv_data這個變數儲存的是一個元組,例如 (b'hahaha', ('192.168.77.1', 8888))
    recv_msg = recv_data[0]
    send_addr = recv_data[1]
    # print("%s 傳送了:%s" % (str(send_addr), recv_msg.decode("utf-8")))  # linux傳送的資料用utf8解碼
    print("%s 傳送了:%s" % (str(send_addr), recv_msg.decode("gbk")))  # windows傳送的資料用gbk解碼

    udp_socket.close()


if __name__ == "__main__":
    main()

udp接發資料總結

傳送資料的流程:

  1. 建立套接字
  2. 傳送資料
  3. 關閉套接字

接收資料的流程:

  1. 建立套接字
  2. 繫結本地自己的資訊,ip和埠
  3. 接收資料
  4. 關閉套接字

埠繫結的問題

  • 如果在你傳送資料時,還沒有繫結埠,那麼作業系統就會隨機給你分配一個埠,迴圈傳送時用的是同一個埠;
  • 也可以先繫結埠,再發送資料。

udp傳送訊息時自己繫結埠示例

import socket


def main():
    # 建立一個udp套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 繫結埠
    udp_socket.bind(('192.168.13.1', 8080))
    while 1:
        # 從鍵盤獲取要傳送的資料
        send_data = input("請輸入你要傳送的資料:")
        if send_data == "exit":
            break
        # 使用套接字收發資料
        udp_socket.sendto(send_data.encode("utf-8"), ("193.168.77.1", 8080))

    # 關閉套接字
    udp_socket.close()  # 按ctrl+c退出


if __name__ == "__main__":
    main()

但應注意,同一埠在同一時間不能被兩個不同的程式同時使用;

單工,半雙工,全雙工

單工半雙工全雙工的理解

單工:
只能單向傳送資訊,別人接收,別人不能回覆訊息,比如廣播;

半雙工:
兩個人都能發訊息,但是在同一時間只能有一個人發訊息,比如對講機;

全雙工:
兩個人都能發訊息,能同時發,比如打電話;

udp使用同一套接字收且發資料

"""socket套接字是全雙工"""
import socket


def main():
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_socket.bind(('192.168.13.1', 8080))
    # 讓使用者輸入要傳送的ip地址和埠
    dest_ip = input("請輸入你要傳送資料的ip地址:")
    dest_port = int(input("請輸入你要傳送資料的埠號:"))

    # 從鍵盤獲取要傳送的資料
    send_data = input("請輸入你要傳送的資料:")
    # 使用套接字收發資料
    udp_socket.sendto(send_data.encode("utf-8"), (dest_ip, dest_port))
    # 套接字可以同時 收發資料;
    recv_data = udp_socket.recvfrom(1024)
    print(recv_data)

    # 關閉套接字
    udp_socket.close()  # 按ctrl+c退出


if __name__ == "__main__":
    main()

在這裡體現不出來socket是全雙工,因為現在直譯器只能按照流程,一步一步走下去,後面學習了程序執行緒協程就可以做到了。

tcp

tcp-可靠傳輸

tcp採取的機制

  1. 採用傳送應答機制
  2. 超時重傳
  3. 錯誤校驗
  4. 流量控制和阻塞管理

tcp與udp的區別

  1. tcp更安全可靠,udp相對沒那麼安全可靠;
  2. 面向連線
  3. 有序資料傳輸
  4. 重發丟失的資料
  5. 捨棄重複的資料包
  6. 無差錯的資料傳輸
  7. 阻塞/流量控制

tcp,udp應用場景

tcp應用場景:下載,傳送訊息
udp應用場景:電話,視訊直播等

tcp客戶端

tcp客戶端傳送資料

import socket


def main():
    # 1.建立tcp的套接字
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.連結伺服器
    tcp_socket.connect(('193.168.11.1', 8080))
    # 3.傳送/接收訊息
    send_data = input("請輸入你要傳送的訊息:")
    tcp_socket.send(send_data.encode("utf-8"))
    # 4.關閉套接字
    tcp_socket.close()


if __name__ == "__main__":
    main()

tcp伺服器

監聽套接字,專門用來監聽的;
accept會對應新建立的套接字,當監聽套接字收到一個請求後,將該請求分配給新套接字,由此監聽套接字可以繼續去監聽了,而新套接字則為該胡克段服務。

import socket


def main():
    # 建立tcp套接字
    tcp_service_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_service_socket.bind(('', 8080))
    # 讓預設的套接字由主動變為被動
    tcp_service_socket.listen(128)

    # 等待客戶端的連結
    new_client_socket, client_addr = tcp_service_socket.accept()
    print("連結的客戶端地址為:", client_addr)
    # 接收客戶端傳送過來的請求
    recv_data = new_client_socket.recvfrom(1024)
    print(recv_data)
    # 給客戶端回送訊息
    new_client_socket.send("hahahah".encode("utf-8"))

    new_client_socket.close()
    tcp_service_socket.close()


if __name__ == '__main__':
    main()

listen裡面的引數,表示同時只允許128個連結訪問。

QQ不繫結埠的執行原理-擴充套件

udp和tcp並用;
使用QQ,先登入,登入後告訴騰訊伺服器此QQ執行的埠,發訊息時,通過騰訊伺服器轉發給另一個QQ;
不繫結埠也有一個好處,就是允許多開,即一個電腦上可以執行多個QQ;

recv和recvfrom的區別

recvfrom裡面不僅有發過來的資料,還有發過來資料的人的資訊;
recv裡面就只有資料;

tcp客戶端服務端流程梳理

tcp伺服器流程梳理

  1. 建立伺服器套接字
  2. 繫結本地資訊
  3. 讓預設的套接字由主動變為被動
  4. 等待客戶端的連結,堵塞
  5. 被客戶端連結後,建立一個新的客服套接字為客戶端服務;
  6. 接收客戶端傳送的訊息,堵塞
  7. 接收客戶端傳送的訊息後,給客戶端回訊息
  8. 關閉客服套接字,關閉服務端套接字

tcp注意點

  1. tcp伺服器一般情況下都需要繫結,否則客戶端找不到這個伺服器。
  2. tcp客戶端一般不繫結,因為是主動連結伺服器,所以只要確定好伺服器的ip, port等資訊就好,本地客戶端可以隨機。
  3. tcp伺服器通過listen可以將socket創建出來的主動套接字變為被動的,這是做tcp伺服器時必須要做的。
  4. 當客戶端需要連結伺服器時,就需要使用connect進行連結, udp是不需要連結的而是直接傳送,但是tcp必須先連結,只有連結成功才能通訊。

  5. 當一個tcp客戶端連線伺服器時,伺服器端會有1個新的套接字,這個套接字用來標記這個客戶端,單獨為這個客戶端服務。

  6. liston後的套接字是被動套接字,用來接收新的客戶端的連結請求的,而accept返回的新套接字是標記這個新客戶端的。

  7. 關閉isten後的套接字意味著被動套接字關閉了,會導致新的客戶端不能夠連結伺服器,但是之前已經連結成功的客戶端正常通訊。

  8. 關閉accept返回的套接字意味著這個客戶端已經服務完畢。

9.當客戶端的套接字呼叫close後.伺服器端會recv解堵塞,並且返回的長度為0,因此伺服器可以通過 返回資料的長度來區別客戶端是否已經下線。

tcp應用案例

示例1-為一個使用者辦理一次業務:

"""可以理解為銀行一個客服為排隊的人員辦理業務"""

import socket


def main():
    # 1.建立tcp套接字
    tcp_service_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.繫結本地資訊
    tcp_service_socket.bind(('', 8080))

    # 3.讓預設的套接字由主動變為被動
    tcp_service_socket.listen(128)
    while 1:
        # 4.等待客戶端的連結
        new_client_socket, client_addr = tcp_service_socket.accept()
        print("連結的客戶端地址為:", client_addr)
        # 接收客戶端傳送過來的請求
        recv_data = new_client_socket.recvfrom(1024)
        print(recv_data)
        # 給客戶端回送訊息
        new_client_socket.send("hahahah".encode("utf-8"))
        # 關閉套接字
        new_client_socket.close()

    tcp_service_socket.close()


if __name__ == '__main__':
    main()

示例2-為同一使用者服務多次並判斷一個使用者是否服務完畢:

"""可以理解為銀行一個客服為排隊的人員辦理業務"""

import socket


def main():
    # 1.建立tcp套接字
    tcp_service_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.繫結本地資訊
    tcp_service_socket.bind(('', 8080))

    # 3.讓預設的套接字由主動變為被動
    tcp_service_socket.listen(128)
    while 1:
        # 4.等待客戶端的連結
        new_client_socket, client_addr = tcp_service_socket.accept()
        print("連結的客戶端地址為:", client_addr)
        # 迴圈目的:為同一個客戶服務多次
        while 1:
            # 接收客戶端傳送過來的請求
            recv_data = new_client_socket.recvfrom(1024)
            print(recv_data)
            # 如果recv解堵塞,那麼有兩種方式
            # 1.客戶端發了資料過來
            # 2.客戶端呼叫了close
            if recv_data:
                # 給客戶端回送訊息
                new_client_socket.send("hahahah".encode("utf-8"))
            else:
                break
        # 關閉套接字
        new_client_socket.close()

    tcp_service_socket.close()


if __name__ == '__main__':
    main()

示例3-tcp檔案下載客戶端和服務端:

檔案下載客戶端

import socket


def main():
    # 1.建立套接字
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.獲取伺服器的ip,port
    dest_ip = input("請輸入你要連結的伺服器ip:")
    dest_port = input("請輸入你要連結的埠:")
    # 3.連結伺服器
    tcp_socket.connect((dest_ip, dest_port))

    # 4.獲取下載的檔名字
    want_file = input("請輸入你要下載的檔案:")
    # 5.將檔名字傳送到伺服器
    tcp_socket.send(want_file.encode("utf-8"))

    # 6.接收要下載的檔案
    file_data = tcp_socket.recv(1024)
    # 7.將接收檔案的資料寫入一個檔案中
    if file_data:
        with open("[復件]" + want_file, "wb") as f:
            f.write(file_data)

    # 8.關閉套接字
    tcp_socket.close()
    pass


if __name__ == '__main__':
    main()

檔案下載服務端

import socket


def send_file2client(new_socket, client_addr):
    # 1.接受客戶端傳送過來的 要下載的檔名
    want_file = new_socket.recv(1024).decode("utf-8")
    print("客戶端 %s 要接收的檔案為:%s" % (str(client_addr), want_file))
    # 2.讀取檔案資料
    file_data = None
    try:
        f = open(want_file, "rb")
        file_data = f.read()
        f.close()
    except Exception as e:
        print("你要下載的檔案 %s 不存在" % want_file)

    # 3.傳送檔案的資料給客戶端
    if file_data:
        new_socket.send(file_data)


def main():
    # 1.建立套接字
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.繫結本地資訊
    tcp_socket.bind(('', 8080))
    # 3.套接字被動接受 listen
    tcp_socket.listen(128)
    while 1:
        # 4.等待客戶端的連結 accept
        new_socket, client_addr = tcp_socket.accept()
        # 5.呼叫函式傳送檔案到客戶端
        send_file2client(new_socket, client_addr)
        # 7.關閉套接字
        new_socket.close()

    tcp_socket.close()


if __name__ == '__main__':
    main()