Python3 與 C# 網絡編程之~ 網絡基礎篇

最新版本查看:https://www.cnblogs.com/dotnetcrazy/p/9919202.htmlhtml

入門篇

官方文檔:https://docs.python.org/3/library/ipc.html(進程間通訊和網絡)python

實例代碼:https://github.com/lotapp/BaseCode/tree/master/python/6.netgit

1.概念

1.1.Python方向

已經講了不少Python的知識了,那Python能幹啥呢?這個是我眼中的Python程序員

Python方向github

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

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

多句嘴,通常狀況下須要什麼庫就去官方看下,沒有再考慮第三方: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 Socket
3.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()

輸出:(你把端口換成經常使用端口列表就知道服務器開了哪些服務了

dnt@MZY-PC:~/桌面/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]
dnt@MZY-PC:~/桌面/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
相關文章
相關標籤/搜索