1. 程式人生 > >Python3 與 C# 網路程式設計之~ 網路基礎篇

Python3 與 C# 網路程式設計之~ 網路基礎篇

最新版本檢視:https://www.cnblogs.com/dotnetcrazy/p/9919202.html

入門篇

官方文件:https://docs.python.org/3/library/ipc.html(程序間通訊和網路)

例項程式碼:https://github.com/lotapp/BaseCode/tree/master/python/6.net

1.概念

1.1.Python方向

已經講了很多Python的知識了,那Python能幹啥呢?這個是我眼中的Python

Python方向

  1. 早期方向
    • Web全棧
  2. 擅長專欄
    • 爬蟲系列
    • 資料分析
    • 人工智慧
    • 物聯網系lot萬物互聯)
    • 自動化運維安全測試
  3. 其他系列
    • 遊戲開發(最近很火)

如果想專攻Web爬蟲物聯網遊戲等等方向,網路這塊是硬條件,So ==> 不要不急,咱們繼續學習~

多句嘴,一般情況下需要什麼庫就去官方看下,沒有再考慮第三方:https://docs.python.org/3/library

1.2.拙見一點點

技術前景:(注意加粗方向)

  1. Python
    1. 最常用:Data
    2. 最看好:LoT
    3. 最火是:AI
    4. 經典是:Web
    5. 壟斷是:System
  2. Web
    1. 最看好:小程式
    2. 最常見:移動端
    3. 最火是:Web端
  3. Go高併發區塊鏈)、C(基礎
  4. NetCoreWebAPIEFCore

總的來說:Python最吃香,Go最有潛力,Web必不可少,NetCore價效比高

現在基本沒有單一方向的程式設計師了,如果有可以默默思考幾分鐘,一般都是JS and Python and (Go or NetCore)【二選一】


其他行業:(僅代表逆天個人看法

  1. 設計師
    1. 影視製作(剪輯師、合成師、特效師)【目前最火,價效比很高】
    2. 修圖師(商業修片、影樓後期)【大咖特別多,創業很吃香】
    3. UI|UE(最容易找工作)
    4. 平面設計(最常見)
    5. 室內設計(高手很吃香)
  2. 教育
    1. **幼兒程式設計中醫課**最火
    2. 琴棋書畫武+國學需求頗高
    3. 英語一直是出國必學
  3. 營銷新媒體+短視訊
  4. 旅遊出國遊

1.2.分層模型

1.OSI 7層模型

  1. 物理層:物理裝置標準,主要作用就是傳輸位元流(資料為bit)eg:網線介面、光纖介面、各種傳輸介質的傳輸速率
    • 雙絞線,光纖(硬體)
  2. 資料鏈路層:對物理層的校驗(是否有丟失、錯誤)
    • 資料的傳輸和資料檢測(網絡卡層)
  3. 網路層:指定傳輸過程中的路徑。eg:IP
    • 為資料包選擇路由(保證資料傳達)
  4. 傳輸層:定義了傳輸資料的協議和埠號(主要就是攜帶了埠號,這樣可以找到對應的程序)
    • 提供端對端的介面,eg:TCP、UDP
  5. 會話層:通過傳輸層,在端與端之間(埠)建立資料傳輸通道(裝置之間可以通過IP、Mac、主機名相互認識)
    • 解除或者建立和別的節點之間的聯絡
  6. 表示層:保證一個系統應用發的訊息可以被另一個系統應用讀取到。eg:兩個應用傳送的訊息格式不同(eg:UTF和ASCII各自表示同一字元),有必要時會以一種通用格式來實現不同資料格式之間的轉換
    • 資料格式化、程式碼轉化、資料加密
  7. 應用層:為使用者的應用程式提供網路服務
    • 檔案傳輸、電子郵箱、檔案服務、虛擬終端

我用PPT畫了個圖:( ) 1.分層模型.png

2.TCP/IP 4層模型

  1. 網路介面層:(物、數
    • eg:乙太網幀協議
  2. 網路層
    • eg:IP、ARP協議
  3. 傳輸層
    • eg:TCP、UDP協議
  4. 應用層:(會、表、應)我們基本上都是關注這個
    • eg:FTP、SSH、HTTP協議...

1.3.協議相關

計算機和計算機網路通訊前達成的一種約定,舉個例子:以漢語為交流語言 1.協議定義.png

再舉個傳送檔案的例子,PPT做個動畫:(自定義協議-檔案傳輸演示) 1.檔案傳輸演示.gif

B/S基本上都是HTTP協議,C/S開發的時候有時會使用自己的協議,比如某大型遊戲,比如很多框架都有自己的協議:

  1. Redis的redis://
  2. Dubbo的dubbo://協議

總的來說,基本上都是HTTP協議,對效能要求高的就使用TCP協議,更高效能要求就自己封裝協議了,比如騰訊在UDP基礎上封裝了自己的協議來保證通訊的可靠性

資料包的封裝

先看一個老外的動畫(忽略水印廣告):https://v.qq.com/x/page/w01984zbrmy.html

中文版可以點選微信公眾號的原文連結下載(課外拓展也有貼)

以TCP/IP四層協議為例:資料包的逐層封裝解包都是作業系統來做的,我們只管應用層

傳送過程:

  1. 傳送訊息
  2. 應用層添加了協議頭
  3. 傳輸層新增TCP段首
  4. 網路層新增IP報頭
  5. 網路介面層(鏈路層)新增幀頭幀尾

PPT動畫示意: 1.傳輸.gif

接收過程:

  1. 去除鏈路層的幀頭幀尾
  2. 去除網路層**IP報頭**
  3. 去除傳輸層**TCP段首**
  4. 去除應用層的協議頭
  5. 獲取到資料

PPT動畫示意: 2.解包.gif

我們下面按照解包順序簡單說說各種格式

1.乙太網幀格式

先看一下這個是啥?用上面動畫內容表示: 1.乙太網幀格式是啥.png

乙太網幀協議根據MAC地址完成資料包傳遞

如果只知道IP,並不知道MAC地址,可以使用ARP請求來獲取:

  • ARP資料報:根據IP獲取MAC地址(網絡卡編號)
  • ARP只適合IPv4,IPv6ICMPV6來代替ARP
  • TCP/IP模型中,ARP協議屬於IP層;在OSI模型中,ARP協議屬於鏈路層

PPT畫一張圖:1bit = 8byte(1位元組=8位) 1.乙太網幀格式.png

上圖資料最小46位元組,而ARP就28位元組,所以需要填充(PAD)18個無用位元組

課後思考:根據ARP原理想想ARP欺騙到底扎回事?(IP進行ARP請求後會快取,快取失效前不會再去ARP請求)

擴充套件:

  1. RARP 是反向地址轉換協議,通過 MAC 地址確定 IP 地址
  2. 真實IP在網路層的IP協議之中,乙太網幀中的IP是下一跳的IP地址(路由)
  3. 每到一個路由都要解網路層的包(知道到底需要獲取哪個IP)
  4. MAC地址就是硬體地址,廠商向全球組織申請唯一編號(類似於身份證)
  5. 最後附上手畫的ARP資料報圖示:(一般都不是一步得到MAC的,多數都是經過一個個路由節點最終獲取到MAC)

1.ARP.png

2.IP段格式

先貼一IP段格式圖片(網路): 1.IP報.png

我們在這不去詳細講解,擴充套件部分有課後拓展,我就說一個大多數人困惑的點:

檢視IP資訊的時候經常會看到192.168.36.235/24,這個**/24**一直爭議很大

我們來簡單解釋一下:IP為192.168.36.235

  1. 192.168.36:網路標識
  2. 235:主機標識
  3. /24標識從頭數到多少位為止屬於網路標識(剩下的就是可分配的主機數了)
    • 二進位制表示為:11111111 11111111 11111111 00000000(24個1)
    • 翻譯成子網掩碼就是:255.255.255.0(/多少就數多少個1,然後轉化)
    • 表示可以有255個ip用來自行分配(記得去除路由之類的佔用)

擴充套件:IP屬於面向無連線行(IP協議不保證傳輸的可靠性,資料包在傳輸過程中可能丟失,可靠性可以在上層協議或應用程式中提供支援)

面向連線面向無連線區別如圖:(圖片來自網路1.面向有無連線.png


預告

關於TCP和UDP的內容下次繼續~

課外拓展:

圖解TCP/IP第五版
連結: https://pan.baidu.com/s/1C4kpNd2MvljxfwTKO082lw 提取碼: 7qce

Python網路程式設計第三版
Code:https://github.com/brandon-rhodes/fopnp
PDF:連結: https://pan.baidu.com/s/1jhW-Te-GCEFKrZVf46S_Tw 提取碼: d7fw

網路基礎-含書籤(網路文件)
連結: https://pan.baidu.com/s/1WZ1D4BthA4qBk2QXBAjm4w 提取碼: jmdg

老外講解網路資料包解析:
下載:https://pan.baidu.com/s/1uUjahs_b05y9Re9ROtzzIw
中文:http://video.tudou.com/v/XMjE3MTg0NzkzNg==.html
英文:http://video.tudou.com/v/XMTkyNjU5NDYwOA==.html

2.UDP

例項程式碼:https://github.com/lotapp/BaseCode/tree/master/python/6.net/1.UDP

UDP是無連線的傳輸協議,不保證可靠性。使用UDP協議的應用程式需要自己完成丟包重發、訊息排序等工作(有點像寄信)

2.1.UDP傳送訊息

引入案例

看個UDP的簡單案例:

import socket

def main():

    # AF_INET ==> IPV4;SOCK_STREAM ==> 型別是TCP,stream 流
    # SOCK_DGRAM ==> 型別是UDP,dgram 資料報、資料報套接字
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_sock:
        udp_sock.sendto("大兄弟,你好啊".encode("utf-8"), ("192.168.36.235", 8080))
    print("over")

if __name__ == '__main__':
    main()

接收到的訊息:這時候埠是隨機的 2.UDP接收訊息

看起來程式碼還挺麻煩,我稍微分析下你就知道對比其他語言真的太簡單了:

標識:

  1. AF_INET ==> IPV4
  2. SOCK_DGRAM ==> 型別是UDP
  3. SOCK_STREAM ==> 型別是TCP

程式碼三步走

  1. 建立 udp_sock=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  2. 傳送 udp_sock.sendto(Bytes內容,(IP,Port)) 接收:udp_sock.recvfrom(count)
  3. 關閉 udp_sock.close()

埠繫結

藉助除錯工具(點我下載)可以知道:上面程式每次執行,不固定 2.UDP隨機埠.png

那怎麼使用固定埠呢?==> udp_socket.bind(('', 5400))

import socket

def main():
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket:
        # 繫結固定埠
        udp_socket.bind(('', 5400))
        # 傳送訊息
        udp_socket.sendto("小明,你知道小張的生日嗎?\n".encode("utf-8"),
                          ("192.168.36.235", 8080))
    print("over")

if __name__ == '__main__':
    main()

訊息圖示:nc -ul 8080nc -l是監聽TCP) 2.nc監聽UDP.png

除錯工具: 2.UDP繫結埠.png

2.2.UDP接收訊息

先看一個簡單版本的:udp_socket.recvfrom(1024)

from socket import socket, AF_INET, SOCK_DGRAM

def main():
    with socket(AF_INET, SOCK_DGRAM) as udp_socket:
        # 繫結埠
        udp_socket.bind(('', 5400))
        while True:
            # 傳送訊息
            udp_socket.sendto("你可以給我離線留言了\n".encode("utf-8"),
                              ("192.168.36.235", 8080))
            # 接收訊息(data,(ip,port))
            data, info = udp_socket.recvfrom(1024)
            print(f"[來自{info[0]}:{info[1]}的訊息]:\n{data.decode('utf-8')}")

if __name__ == '__main__':
    main()

圖示:接收訊息(data,(ip,port)) 2.udp_recv.gif


題外話(Nmap)

其實如果你使用Nmap來掃描的話並不能發現nc開啟的UDP埠: 2.nmap的UDP掃描.gif

稍微解釋一下:掃描其實就是發了幾個空訊息過去

  1. -sU代表掃描UDP,-sT代表掃描TCP
  2. -Pn 這個主要是針對有些伺服器禁用ping的處理(ping不通也嘗試)
  3. -p 指定埠號,如果是所有埠可以使用-p-
  4. sudo是因為在Ubuntu下沒許可權,kali下可以直接使用nmap

可能有人對nc輸出的*你可以給離線留意了*有疑惑,其實就是在給5400埠發空訊息的時候~True迴圈了兩次

來張對比圖: 2.nc找不到.gif

掃描TCP和UDP埠sudo nmap -sTU 192.168.36.235 -Pn

課後擴充套件

NC命令擴充套件:https://www.cnblogs.com/nmap/p/6148306.html

Nmap基礎:https://www.cnblogs.com/dunitian/p/5074784.html

收放自如

如果還是用True迴圈來實現:

from socket import socket, AF_INET, SOCK_DGRAM

def main():
    with socket(AF_INET, SOCK_DGRAM) as udp_socket:
        # 繫結埠
        udp_socket.bind(('', 5400))
        while True:
            msg = input("請輸入傳送的內容:")
            if msg == "dotnetcrazy":
                break
            else:
                udp_socket.sendto(
                    msg.encode("utf-8"), ("192.168.36.235", 8080))

            data, info = udp_socket.recvfrom(1024)
            print(f"[來自{info[0]}:{info[1]}的訊息]:\n{data.decode('utf-8')}")

if __name__ == '__main__':
    main()

你會發現,訊息不能輪流傳送,只能等對方方式後再發,雖然有處理方式,但太麻煩,這時候就可以使用我們之前說的多執行緒來改寫一下了:

from socket import socket, AF_INET, SOCK_DGRAM
from multiprocessing.dummy import Pool as ThreadPool

def send_msg(udp_socket):
    while True:
        msg = input("輸入需要傳送的訊息:\n")
        udp_socket.sendto(msg.encode("utf-8"), ("192.168.36.235", 8080))

def recv_msg(udp_socket):
    while True:
        data, info = udp_socket.recvfrom(1024)
        print(f"[來自{info[0]}:{info[1]}的訊息]:\n{data.decode('utf-8')}")

def main():
    # 建立一個Socket
    with socket(AF_INET, SOCK_DGRAM) as udp_socket:
        # 繫結埠
        udp_socket.bind(('', 5400))

        # 建立一個執行緒池
        pool = ThreadPool()

        # 接收訊息
        pool.apply_async(recv_msg, args=(udp_socket, ))

        # 傳送訊息
        pool.apply_async(send_msg, args=(udp_socket, ))

        pool.close()  # 不再新增任務
        pool.join()  # 等待執行緒池執行完畢
    print("over")

if __name__ == '__main__':
    main()

輸出:(就一個注意點~socket在pool之後關閉2.收放自如.gif


2.3.手寫UDP網路除錯工具

除錯工具功能比較簡單,我們手寫一個UDP版的:

from socket import socket, AF_INET, SOCK_DGRAM
from multiprocessing.dummy import Pool as ThreadPool

def get_port(msg):
    """獲取使用者輸入的埠號"""
    while True:
        port = input(msg)
        try:
            port = int(port)
        except Exception as ex:
            print(ex)
        else:
            return port  # 沒有錯誤就退出死迴圈

def recv_msg(udp_socket):
    """接收訊息"""
    while True:
        data, info = udp_socket.recvfrom(1024)
        print(f"[來自{info[0]}:{info[1]}的訊息]:\n{data.decode('utf-8')}")

def send_msg(udp_socket):
    """傳送訊息"""
    ip = input("請輸入對方IP:")
    port = get_port("請輸入對方埠號:")
    while True:
        msg = input("請輸入傳送的訊息:\n")
        udp_socket.sendto(msg.encode("utf-8"), (ip, port))

def main():
    with socket(AF_INET, SOCK_DGRAM) as udp_socket:
        # 繫結埠
        udp_socket.bind(('', get_port("請輸網路助手的埠號:")))
        # 建立一個執行緒池
        pool = ThreadPool()
        # 接收訊息
        pool.apply_async(recv_msg, args=(udp_socket, ))
        # 傳送訊息
        pool.apply_async(send_msg, args=(udp_socket, ))

        pool.close()
        pool.join()

if __name__ == '__main__':
    main()

CentOSIPPort(192.168.36.123:5400) 2.UDP網路助手.png

演示:(多PC演示) 2.UDPTool.gif

簡單說下本機IP的繫結:

Net裡面習慣使用localhost,很多人不知道到底是啥,其實你開啟host檔案就可以看到 ==> 127.0.0.1被重定向為localhost,在Linux裡面也是這樣的,每個PC對應的都是lo迴環地址: 2.lo.png

本機通訊時,對方ip就可以使用127.0.0.1了,當然了繫結本機ip的時候也可以使用127.0.0.1bind(('',))中的空其實填的就是這個)(很多地方也會使用0.0.0.0)

_LOCALHOST    = '127.0.0.1' # 看這
_LOCALHOST_V6 = '::1'

   def socketpair(family=AF_INET, type=SOCK_STREAM, proto=0):
        if family == AF_INET:
            host = _LOCALHOST # 看這
        elif family == AF_INET6:
            host = _LOCALHOST_V6
        ....
        
        lsock = socket(family, type, proto)
        try:
            lsock.bind((host, 0)) # 看這
            lsock.listen()
            ...

2.4.NetCore版

快速實現一下:

using System.Net;
using System.Text;
using System.Net.Sockets;

namespace netcore
{
    class Program
    {
        static void Main(string[] args)
        {
            // UDP通訊
            using (var udp_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
            {
                var ip_addr = IPAddress.Parse("192.168.36.235");

                // 繫結本地埠
                udp_socket.Bind(new IPEndPoint(ip_addr, 5400));
                // UDP傳送訊息
                int i = udp_socket.SendTo(Encoding.UTF8.GetBytes("小明你好啊~"), new IPEndPoint(ip_addr, 8080));
                Console.WriteLine($"傳送計數:{i}");
            }
            Console.WriteLine("over");
        }
    }
}

3.TCP

示例程式碼:https://github.com/lotapp/BaseCode/tree/master/python/6.net/2.TCP

TCP是一種面向連線的、可靠的協議,TCP傳輸的雙方需要首先建立連線,之後由TCP協議保證資料收發的可靠性,丟失的資料包自動重發,上層應用程式收到的總是可靠的資料流,通訊之後關閉連線(有點像打電話)

用過下載軟體的可能遇到過一種‘Bug’ ==> 很多人為了防止自己本地檔案納入共享大軍,一般都是直接把網路上傳給禁了,然後發現檔案經常出問題?

其實這個就是TCP的一個應用,檔案一般都很大,所以進行分割後批量下載,那少量的網路上傳其實是為了校驗一下檔案 ==> 正確做法是限制上傳速度而不是禁止(學生時代那會還經常蛋疼這個問題,現在想想還挺好玩的O(∩_∩)O

大多數連線都是可靠的TCP連線。建立TCP連線時,主動發起連線的叫客戶端,被動響應連線的叫伺服器

上面那個例子裡,我們的下載工具就是客戶端,每一小段檔案接收完畢後都會向伺服器傳送一個完成的指令來保證檔案的完整性

3.1.TCP客戶端

來看一個簡單的入門案例:

from socket import socket

def main():
    # 預設就是建立TCP Socket
    with socket() as tcp_socket:
        # 連線伺服器(沒有返回值)
        tcp_socket.connect(("192.168.36.235", 8080))
        # 傳送訊息(返回傳送的位元組數)
        tcp_socket.send("小張生日快樂~".encode("utf-8"))
        # 接收訊息
        msg = tcp_socket.recv(1024)
        print(f"伺服器:{msg.decode('utf-8')}")

if __name__ == '__main__':
    main()

輸出:(socket()預設就是建立TCP Socket3.tcp_client.gif

概括來說:

  1. TCP,有點像打電話,先撥號連通了(connect)才能通訊(sendrecv),之後的通訊不用再撥號連通了
  2. UDP,有點像寄信封,每次寄過去都不確定能不能收到,每次通訊都得寫地址(ip+port)

程式碼四步走:(TCP客戶端其實建立Socket之後connect一下伺服器就OK了)

  1. 建立:tcp_sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  2. 連線:tcp_sock.connect((IP, Port))
  3. 傳送:tcp_sock.send(Bytes內容) 接收:tcp_sock.recv(count)
  4. 關閉:tcp_sock.close()

模擬HTTP

from socket import socket

def get_buffer(tcp_socket):
    buffers = b''
    while True:
        b = tcp_socket.recv(1024)
        if b:
            buffers += b
        else:
            break
    # 返回bytes
    return buffers

def main():
    with socket() as tcp_socket:
        # 連線伺服器
        tcp_socket.connect(("dotnetcrazy.cnblogs.com", 80))
        # 傳送訊息(模擬HTTP)
        tcp_socket.send(
            b'GET / HTTP/1.1\r\nHost: dotnetcrazy.cnblogs.com\r\nConnection: close\r\n\r\n'
        )
        # 以"\r\n\r\n"分割一次
        header, data = get_buffer(tcp_socket).split(b"\r\n\r\n", 1)
        print(header.decode("utf-8"))
        with open("test.html", "wb") as f:
            f.write(data)
    print("over")

if __name__ == '__main__':
    main()

輸出:(test.html就是頁面原始碼)

HTTP/1.1 200 OK
Date: Thu, 01 Nov 2018 03:10:48 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 20059
Connection: close
Vary: Accept-Encoding
Cache-Control: private, max-age=10
Expires: Thu, 01 Nov 2018 03:10:58 GMT
Last-Modified: Thu, 01 Nov 2018 03:10:48 GMT
X-UA-Compatible: IE=10
X-Frame-Options: SAMEORIGIN
over

注意\r\nConnection:closesplit("",分割次數)


3.2.TCP服務端

服務端程式碼相比於UDP,多了一個監聽和等待客戶端,其他基本上一樣:

客戶端Code:(如果你想固定埠也可以繫結一下Port)

from socket import socket

def main():
    # 預設就是建立TCP Socket
    with socket() as tcp_socket:
        # 連線伺服器(沒有返回值)
        tcp_socket.connect(("192.168.36.235", 8080))

        print("Connected TCP Server...")  # 連線提示

        # 傳送訊息(返回傳送的位元組數)
        tcp_socket.send("小張生日快樂~\n".encode("utf-8"))
        # 接收訊息
        msg = tcp_socket.recv(1024)
        print(f"伺服器:{msg.decode('utf-8')}")

if __name__ == '__main__':
    main()

服務端Code:

from socket import socket

def main():
    with socket() as tcp_socket:
        # 繫結埠(便於客戶端找到)
        tcp_socket.bind(('', 8080))
        # 變成被動接收訊息(監聽)
        tcp_socket.listen()  # 不指定連線最大數則會設定預設值

        print("TCP Server is Running...")  # 執行後提示

        # 等待客戶端發信息
        client_socket, client_addr = tcp_socket.accept()

        with client_socket:
            # 客戶端連線提示
            print(f"[來自{client_addr[0]}:{client_addr[1]}的訊息]\n")

            # 接收客戶端訊息
            data = client_socket.recv(1024)
            print(data.decode("utf-8"))

            # 回覆客戶端
            client_socket.send("知道了".encode("utf-8"))

if __name__ == '__main__':
    main()

輸出:(先執行服務端,再執行客戶端。客戶端發了一個生日快樂的祝福,服務端回覆了一句) 3.tcp_server.gif

3.2.TCP服務端除錯助手

如果像上面那般,並不能多客戶端通訊 3.bug.png

這時候可以稍微改造一下:

客戶端:

from time import sleep
from socket import socket
from multiprocessing.dummy import Pool

def send_msg(tcp_socket):
    with tcp_socket:
        while True:
            try:
                tcp_socket.send("小明同志\n".encode("utf-8"))
                sleep(2)  # send是非阻塞的
                print("向伺服器問候了一下")
            except Exception as ex:
                print("服務端連線已斷開:", ex)
                break

def recv_msg(tcp_socket):
    with tcp_socket:
        while True:
            # 這邊可以不捕獲異常:
            #    服務端關閉時,send_msg會關閉,然後這邊也就關閉了
            try:
                data = tcp_socket.recv(1024)
                if data:
                    print("服務端回覆:", data.decode("utf-8"))
            except Exception as ex:
                print("tcp_socket已斷開:", ex)
                break

def main():
    with socket() as tcp_socket:
        # 連線TCP Server
        tcp_socket.connect(("192.168.36.235", 8080))
        print("Connected TCP Server...")  # 連線提示

        pool = Pool()
        pool.apply_async(send_msg, args=(tcp_socket,))
        pool.apply_async(recv_msg, args=(tcp_socket,))
        pool.close()
        pool.join()

if __name__ == '__main__':
    main()

服務端

伺服器需要同時響應多個客戶端的請求,那麼每個連線都需要一個新的程序或者執行緒來處理

from socket import socket
from multiprocessing.dummy import Pool

def wait_client(client_socket, ip_port):
    with client_socket:
        while True:
            data = client_socket.recv(1024)
            print(f"[來自{ip_port}的訊息]:\n{data.decode('utf-8')}")
            client_socket.send(b"I Know")  # bytes型別

def main():
    with socket() as tcp_socket:
        # 繫結埠
        tcp_socket.bind(('', 8080))
        # 伺服器監聽
        tcp_socket.listen()

        print("TCP Server is Running...")  # 執行後提示

        p = Pool()
        while True:
            # 等待客戶端連線
            client_socket, client_addr = tcp_socket.accept()
            ip_port = f"{client_addr[0]}:{client_addr[1]}"
            print(f"客戶端{ip_port}已連線")
            # 響應多個客戶端則需要多個執行緒來處理
            p.apply_async(wait_client, args=(client_socket, ip_port))

if __name__ == '__main__':
    main()

演示:(死迴圈,Pool都不用管了) 3.正常流程.gif

伺服器掛了客戶端也會自動退出: 3.自動退出.gif

用TCP協議進行Socket程式設計在Python中十分簡單:

  1. 客戶端:主動連線伺服器的IP和指定埠
  2. 伺服器:先監聽指定埠,然後對每一個新的連線建立一個執行緒或程序來處理

3.3.NetCore版

Server版

大體流程和Python一樣:

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace _2_TCP
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var tcp_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                var ip_addr = IPAddress.Parse("192.168.36.235");
                // 伺服器端繫結Port
                tcp_socket.Bind(new IPEndPoint(ip_addr, 8080));
                // 伺服器監聽
                tcp_socket.Listen(5);
                while (true)
                {
                    // 等待客戶端連線
                    var client_socket = tcp_socket.Accept();
                    // 遠端埠
                    var client_point = client_socket.RemoteEndPoint;
                    Task.Run(() =>
                    {
                        while (true)
                        {
                            byte[] buffer = new byte[1024];
                            int count = client_socket.Receive(buffer);
                            Console.WriteLine($"來自{client_socket.RemoteEndPoint.ToString()}的訊息:\n{Encoding.UTF8.GetString(buffer, 0, count)}");
                            client_socket.Send(Encoding.UTF8.GetBytes("知道了~"));
                        }
                    });
                }
            }
        }
    }
}

Client版

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace client
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var tcp_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // 連線伺服器
                tcp_socket.Connect(new IPEndPoint(IPAddress.Parse("192.168.36.235"), 8080));

                while (true)
                {
                    // 傳送訊息
                    tcp_socket.Send(Encoding.UTF8.GetBytes("伺服器你好"));
                    // 接收伺服器訊息
                    byte[] buffer = new byte[1024];
                    int count = tcp_socket.Receive(buffer);
                    Console.WriteLine($"來自伺服器的訊息:{Encoding.UTF8.GetString(buffer, 0, count)}");
                }
            }
        }
    }
}

圖示: 3.netcore.gif

擴充套件

示例程式碼:https://github.com/lotapp/BaseCode/tree/master/python/6.net/3.Ext

上面忘記說了,Socket是可以設定超時時間的,eg:tcp_socket.settimeout(3)

探一探localhost

程式碼不變,如果把TCP客戶端的連線伺服器IP空著或者改成127.0.0.1,咱們再看看效果:tcp_socket.connect(('', 8080))

圖示:(怎麼樣,這回知道本機問啥可以不寫IP了吧) 3.localhost.png

手寫一個埠掃描工具

埠掃描大家不陌生,自己實現一個簡單的TCP埠掃描工具:

from socket import socket
from multiprocessing.dummy import Pool

ip = "127.0.0.1"

def tcp_port(port):
    """IP:服務端IP,Port:服務端Port"""
    with socket() as tcp_socket:
        try:
            tcp_socket.connect((ip, port))
            print(f"[TCP Port:{port} is open]")
        except Exception:
            pass

def main():
    # 檢視系統本地可用埠極限值 cat /proc/sys/net/ipv4/ip_local_port_range
    max_port = 60999
    global ip
    ip = input("請輸入要掃描的IP地址:")
    print(f"正在對IP:{ip}進行埠掃描...")

    pool = Pool()
    pool.map_async(tcp_port, range(max_port))
    pool.close()
    pool.join()

if __name__ == '__main__':
    main()

輸出:(你把埠換成常用埠列表就知道伺服器開了哪些服務了

[email protected]:~/桌面/work/BaseCode/python/6.net/3.Ext python3 1.port_scan.py 
請輸入要掃描的IP地址:192.168.36.235
正在對IP:192.168.36.235進行埠掃描...
[TCP Port:22 is open]
[TCP Port:41004 is open]
[email protected]:~/桌面/work/BaseCode/python/6.net/3.Ext sudo nmap -sT 192.168.36.235 -Pn -p-

Starting Nmap 7.60 ( https://nmap.org ) at 2018-11-02 18:15 CST
Nmap scan report for MZY-PC (192.168.36.235)
Host is up (0.000086s latency).
Not shown: 65534 closed ports
PORT   STATE SERVICE
22/tcp open  ssh

Nmap done: 1 IP address (1 host up) scanned in 2.07 seconds

課後思考

可以自行研究拓展:

  1. 為啥傳送(sendsendto)和接收(recvrecvfrom)都是兩個方法?(提示:方法名阻塞
  2. sendsendall有啥區別?
  3. 有沒有更方便的方式來實現服務端?
  4. 結合內網對映或者ShellCode實現一個遠控

課外拓展:

官方Socket程式設計文件【推薦】
https://docs.python.org/3/library/socket.html

Python核心程式設計之~網路程式設計【推薦】
https://wizardforcel.gitbooks.io/core-python-2e/content/19.html

TCP程式設計知識
https://dwz.cn/dDkXzqcV

網路程式設計-基礎
https://www.jianshu.com/p/55c171ebe5f1

網路程式設計-UDP
https://www.jianshu.com/p/594870b1634b

網路程式設計-TCP
https://www.jianshu.com/p/be36d4db5618

Python總結之 recv與recv_from
https://www.jianshu.com/p/5643e810123f
https://blog.csdn.net/xvd217/article/details/38902081
https://blog.csdn.net/pengluer/article/details/8812333

埠掃描擴充套件:(Python2)
https://thief.one/2018/05/17/1

Python socket藉助ngrok建立外網TCP連線
https://www.jianshu.com/p/913b2013a38f

TCP協議知識:
https://www.cnblogs.com/wcd144140/category/1313090.html