---
# System prepended metadata

title: 網路編程

---

網路編程
===
- [海底光纜](https://zh.wikipedia.org/wiki/%E6%B5%B7%E5%BA%95%E9%9B%BB%E7%BA%9C)
- [通訊衛星](https://zh.wikipedia.org/wiki/%E9%80%9A%E8%A8%8A%E8%A1%9B%E6%98%9F)
- [網際網絡歷史](https://zh.wikipedia.org/wiki/%E4%BA%92%E8%81%94%E7%BD%91%E5%8E%86%E5%8F%B2)
    - [ARPANET](https://zh.wikipedia.org/wiki/ARPANET)
- [TCP/IP](https://zh.wikipedia.org/wiki/TCP/IP%E5%8D%8F%E8%AE%AE%E6%97%8F)
    - 四層
    - 七層
- [區域網路](https://zh.wikipedia.org/wiki/%E5%B1%80%E5%9F%9F%E7%BD%91): 大家把電腦都接上[網路交換器](https://zh.wikipedia.org/wiki/%E7%B6%B2%E8%B7%AF%E4%BA%A4%E6%8F%9B%E5%99%A8)，形成一個區域網路
    - A電腦a進程發訊息>A網卡>A網線>交換機>B網線>B網卡>?
    - Q. 訊息發送到B網卡後, B電腦如何知道要傳給哪個軟體接收?
        - 透過IP傳到電腦, 再透過[通訊埠](https://zh.wikipedia.org/wiki/%E9%80%9A%E8%A8%8A%E5%9F%A0)傳給某進程
    - Q. 為何不直接使用PID？
        - 取得對方電腦的某進程的PID有難度, 而通訊埠的值較為固定
        - 亦即PID可區分同一台電腦的進程, 但不同台的無法
        - 舉例: 12345在A電腦可能是line, B電腦則是Telegram

通訊埠(端口)
---
- 埠號: 0~65535(2^16-1)
    - 知名端口: 0~1023
        - 80: HTTP
        - 20: FTP
        - 22: ssh
    - 動態端口:
- 查看端口 `$ netstat －an`
- 不得重複
    - 端口區分進程, 若有相同端口運行, 訊息會無法正確傳遞
    - 故同os之端口不得重複, A進程使用後釋放前, 其他進程不得使用
    - 攔截數據
        - 可以使用跟軟體(ex.Line)相同端口來攔截數據

[IP](https://zh.wikipedia.org/wiki/IP%E5%9C%B0%E5%9D%80)
---
- [IPv4](https://zh.wikipedia.org/wiki/IPv4)
    - 總共有4Byte -> 2^32^ -> 4G
        - 2^10^=1K; 2^20^=1M; 2^30^=1G;
    - 可分為ABCDE五類
        - A: 前1B不變後3B變
        - B: 前2B不變後2B變
        - C: 前3B不變後1B變
        - D: [多播](https://zh.wikipedia.org/wiki/%E5%A4%9A%E6%92%AD)
            - ex.視訊會議
        - E: 預留研究使用(惟出現IPv6, 故E類沒用了)
    - 前面相同即屬同一網段
    - 以C類為例,可以使用 1~254
        - 1B=8b -> 2^8^=256 -> 0~255
        - 但是0,255不能使用
            - 0: 『網絡號』, ex: 192.168.55.0

            - 255: 『廣播地址』, ex: 192.168.55.255
                - 實例: 上線下線讓別人知道的原理
                - 多播vs廣播?
                    - 在區域內就看得到: 廣播
                    - 指定哪些人看得到: 多播
    - 到現在已不足而發展出[IPv6](https://zh.wikipedia.org/zh-tw/IPv6)
    - IPv5? 開發失敗
    - 私有IP vs 公有IP
        - 私: 內部訪問
            - ex.[路由器](https://zh.wikipedia.org/wiki/%E8%B7%AF%E7%94%B1%E5%99%A8)分配給你的196.128無法上網
        - 公: 全球訪問
    - [localhost](https://zh.wikipedia.org/wiki/Localhost)
        - 127.0.0.1
            - 測試電腦網路有沒有壞
            - `$ ping 127.0.0.1`

[socket](https://zh.wikipedia.org/wiki/%E7%B6%B2%E8%B7%AF%E6%8F%92%E5%BA%A7)
---
`socket.socket(AddressFamily, Type)`
- [TCP](https://zh.wikipedia.org/wiki/%E4%BC%A0%E8%BE%93%E6%8E%A7%E5%88%B6%E5%8D%8F%E8%AE%AE)
    - 慢, 穩定
```python
import socket

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

print 'Socket Created'
```
- [UDP](https://zh.wikipedia.org/wiki/%E7%94%A8%E6%88%B7%E6%95%B0%E6%8D%AE%E6%8A%A5%E5%8D%8F%E8%AE%AE)
    - 快, 不穩定

```python
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

print 'Socket Created'
```
- 小結:
    - IP: 區分電腦
    - 端口: 區分進程
    - 協議: UDP/TCP

- SendMsg
```python
from socket import *

udpSocket = socket.(AF_INET, SOCK_DGRAM)

# socket.socket.(AF, Type).sendto("msg", ("IP", Port))

# python2
# udpSocket.sendto("GodJJ", ("196.168.1.1", 8080))

# python3
udpSocket.sendto(b"GodJJ", ("196.168.1.1", 8080))

# 寫下一個訊息
# UDP每次發訊息都要寫IP,Port
udpSocket.sendto(b"GodJJ2", ("196.168.1.1", 8080))
```
- DynamicPort

`$ vi xx.py`
```python
from socket import *
udpSocket = socket.(AF_INET, SOCK_DGRAM)
udpSocket.sendto(b"GodJJ", ("196.168.1.1", 8080))
```
```$
$ python3 xx.py
$ python3 xx.py
$ python3 xx.py
```
```$
<FROM 192.168.1.2 :12345>: GodJJ
<FROM 192.168.1.2 :54321>: GodJJ
<FROM 192.168.1.2 :24135>: GodJJ

# 每次端口都不一樣
```
- bind port

`$ vi xx.py`
```python
from socket import *

udpSocket = socket(AF_INET, SOCK_DGRAM)

# bind(("IP", port))
# IP通常不填, 表示本機的所有IP都綁
udpSocket.bind(("", 5566))

udpSocket.sendto(b"GodJJ", ("192.168.43.114", 8080))

# 通常接收方才需要綁定, 發送方比較少綁定
# 因為發送不一定要讓人知道哪裡發的
# 但是接收不綁沒人知道要傳到哪
```
```$
$ python3 xx.py
$ python3 xx.py
$ python3 xx.py
```
```$
<FROM 192.168.1.2 :5566>: GodJJ
<FROM 192.168.1.2 :5566>: GodJJ
<FROM 192.168.1.2 :5566>: GodJJ
```
- ReceiveMsg

```python
from socket import *

udpSocket = socket(AF_INET, SOCK_DGRAM)

udpSocket.bind(("", 5566))

recvData = udpSocket.recvfrom(1024) # 這次接收之最大Byte: 1024

print(recvData)
```
```$
$ xx.py
(b'GodNaiNai', ('192.168.1.1', 8080))
```
- Key
    - [單工](https://zh.wikipedia.org/wiki/%E5%96%AE%E5%B7%A5%E9%80%9A%E8%A8%8A): 只能單方面接收, 如收音機
    - 半雙工: 能接能收, 無法同時, 如對講機
    - [雙工](https://zh.wikipedia.org/wiki/%E9%9B%99%E5%B7%A5), 能接能收, 可以同時, 如電話
- socket 屬於雙工
- 解碼(encode)
    - [utf-8](https://zh.wikipedia.org/wiki/UTF-8)
    - [gb2312](https://zh.wikipedia.org/wiki/GB_2312)
```python
from socket import *

updSocket = socket(AF_INET, SOCK_DGRAM)

# dest: destination 目標
destIP = input("IP: ")
destPort = int(input("Port: "))
sendData = input("Msg: ")

# .encode 解碼, 跟 b""是一樣的效果, 但.encode較通用!
#updSocket.sendto(sendData.encode("utf-8"), (destIP, destPort))

# 我使用的接收軟體是簡體且不支援utf8, 簡體常用gb2312 
updSocket.sendto(sendData.encode("gb2312"), (destIP, destPort))
```
```$
$ xx.py
IP: 192.168.43.114
Port: 8080
Msg: Toyz

$ xx.py
IP: 192.168.43.114
Port: 8080
Msg: Toyz喊在

$ xx-1.py
IP: 192.168.43.114
Port: 8080
Msg: Toyz喊在
```
```
<FROM 192.168.1.2 :5566> Toyz
<FROM 192.168.1.2 :7788> å-Šåœ¨
<FROM 192.168.1.2 :1122> Toyz喊在
```

- decode

```python
from socket import *

udnSocket = socket(AF_INET, SOCK_DGRAM)

udnSocket.bind(("", 5566))

recvData = udnSocket.recvfrom(1024)

content, destInfo = recvData
    # In [1]: a = (11,22)
    # In [2]: b,c = a
    # In [3]: b                  
    # Out[3]: 11
    # In [4]: c                  
    # Out[4]: 22
    # 
    # content = recvData[0]
    # destInfo = recvData[1]

# print("%s"%content.decode("utf-8"))
# print("%s"%recvDate[0].decode("utf-8"))
print("%s"%content.decode("gb2312"))
```
- [練習] 聊天室

```python
from socket import *

def main():
    udpSocket = socket(AF_INET, SOCK_DGRAM)

    udpSocket.bind(("", 5566))

    while True:
        recvData = udpSocket.recvfrom(1024)
        content, destInfo = recvData
        print("[%s]: %s"%(str(destInfo), content.decode("utf-8")))

if __name__ == "__main__":
    main()
```
```python
from threading import Thread
from socket import *


def recvData(): 
    udpSocket.bind(("", 5566))
    while True:
        recvMsg = udpSocket.recvfrom(1024)
        print("\r>>%s: %s"%(str(recvMsg[1]), recvMsg[0].decode("utf-8")))
        print("\r<<", end="")

def sendData():
    destIP =  input("IP: ")
    destPort = int(input("Port: "))
    while True:
        msg = input("<<")
        sendMsg = udpSocket.sendto(msg.encode("utf-8"), (destIP, destPort))

udpSocket = None
    # Q. 何時寫None/""/0?
    # A. 空對象: None
    #    str: ""
    #    num: 0

def main():
    global udpSocket
    udpSocket = socket(AF_INET, SOCK_DGRAM)

    tr = Thread(target=recvData)
    ts = Thread(target=sendData)

    tr.start()
    ts.start()

if __name__=="__main__":
    main()
```
wireshark
---
- 數據: 16進制, 每兩個16進制數組成1B, 再以ASCII翻譯到右邊

[TFTP](https://zh.wikipedia.org/wiki/%E5%B0%8F%E5%9E%8B%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE)
---
- key: 
    - c/s 
        - 客戶端client: 需求
        - 服務器server: 供給
    - b/s
        - browser

- 下載
    - 創建空文件
    - 填寫數據
    - 關閉

- 下載上傳雙方如何溝通?
    - TFTP協議
    - ![](https://i.imgur.com/YLiAbTE.png)
        - SendReadRequest要傳給69Port,
        - 接著服務器回傳是以新的隨機Port回傳,
        - 後面的資料傳輸皆是傳向新Port而非69!!
    - ![](https://i.imgur.com/fHyJGCH.gif)

    - 如何確定傳送已完成?
        - Server傳送的數據小於516B(2+2+512)即可得知
        - 如最後一次數據剛好是516B?
        - 那麼一樣不小於516而有下次, 下次會傳4B的檔案過來
    - 如何保證一個數字佔2B?
        - struct.pack("1test.jpg0octet0"), 1跟0都只占1B([ASCII](https://zh.wikipedia.org/wiki/ASCII))
            - 0 = 48 = 00110000
            - 1 = 49 = 00110001
        - [大端小端](https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F#%E5%A4%A7%E7%AB%AF%E5%BA%8F)
            - 例如有一數據0x1122
            - 儲存時有可能是0x11+0x22
            - 亦可能是0x22+0x11
            - 如把低位(0x22)存到低地址, 高位(0x11)存到高地址, 即為小端, 反之為大端
            - 一般PC通常是小端, 大型服務器通常是大端
            - 網絡傳輸一般採用大端
        - struct.pack("!H8sb5sb",1,"test.jpg",0,"octet",0)
            - !: network (= big-endian)
            - H: integer, 2B
            - b: integer, 1B
            - s: bytes

- pack

```python
from socket import *
import struct

sendData = struct.pack("!H8sb5sb",1,"test.jpg",0,"octet",0)

udpSocket = socket(AF_INET, SOCK_DRGAM)
udpSocket.sendto(sendData, ("192.168.1.2", 69))

udpSocket.close()

# 問題: 只有python2能用, 
#      python3會有struct.error: 
#      argument for 's' must be a bytes object
#      
# 問題: 我沒有收到回傳的東西....
```

- unpack

```python
from socket import *
import struct

sendData = struct.pack("!H8sb5sb",1,"test.jpg",0,"octet",0)

result = struct.unpack("!H", sendData[:2])
print(result)
```
```$
$ python xxx.py
(1,)
```

- UDP廣播
    - 種類
        - 單播: 一對一
        - 多播: 一對多
        - 廣播: 一對全
    - 發送方會傳給轉接器, 再由轉接器傳送給各個接收方
    - 訊號會傳送到網路層, 傳輸層會判斷該端口有無開啟, 沒開就不接收
    - Q. 有無端口可以讓所有端口都接收？
        - 沒, 如果有, 大家就都可以塞爆你的電腦了
    - 廣播只有UDP有, TCP沒有
    - [廣播風暴](https://zh.wikipedia.org/wiki/%E5%BB%A3%E6%92%AD%E9%A2%A8%E6%9A%B4)
    - Q. 電腦如何取得IP?
        - 由於電腦不知道誰有能力給自己IP
        - 故當你插線後, 電腦會發送廣播給大家
        - 機房收到廣播後即會傳送IP過來
    - 通訊軟體的上線下線也是廣播
    
```python
from socket import *

dest = ("<broadcast>", 5566)
    # <broadcast>會自動搜尋ip廣播地址(255)

s = socket(AF_INET, SOCK_DRGAM)
# 廣播一定要寫這句話
s.setsockopt(SOL_SOCKET, SO_BROACAST,1)

s.sendto("GodJJ", dest)
```

[tcp](https://zh.wikipedia.org/wiki/%E4%BC%A0%E8%BE%93%E6%8E%A7%E5%88%B6%E5%8D%8F%E8%AE%AE)
---
- 穩定
- 相對UDP慢
- Web服務器通常都用tcp
- 服務器
    - socket>bind>listen>accept
        - socket預設為主動套接字
        - listen將主動轉被動
        - 買手機>差sim卡>鈴聲打開>準備接聽

```python
from socket import *

tcpSocket = socket(AF_INET, SOCK_STREAM)

tcpSocket.bind(("", 5566))

tcpSocket.listen(5)
    # 參數backlog指定同時能處理的最大連接要求
    # 掛一個才補一個

newSocket, clientInfo = tcpSocket.accept()

recvData = newSocket.recv(1024)

print("%s:%s"%(str(clientInfo), recvData.decode("utf-8")))

newSocket.close()
tcpSocket.close()
```
```python
# EOFError: EOF when reading a line
# 還沒解決...

from socket import *
from threading import Thread
from multiprocessing import Process


def sendMsg(newSocket):
    while True:
        Msg = input("<<")
        if Msg == "end":
            break
        else:
            # 接通之後就不用一直寫ip/port了, 打內容即可
            newSocket.send(Msg.encode("utf-8"))

def recvMsg(newSocket):
    while True:
        recvMsg = newSocket.recv(1024)
        if recvMsg == "end":
            print("對方已離線")
            break
        else: 
            print("\r>>%s"%recvMsg.decode("utf-8"))
            print("\r<<", end="")
    newSocket.close()    


def clientDeal(newSocket):

    ts = Thread(target=sendMsg, args=(newSocket,))
    tr = Thread(target=recvMsg, args=(newSocket,))
    ts.start()
    tr.start()


tcpSocket = None

def main():
    global tcpSocket
    tcpSocket = socket(AF_INET, SOCK_STREAM)
    tcpSocket.bind(("", 5567))
    tcpSocket.listen(5)

    while True:
        newSocket, clientInfo = tcpSocket.accept()
        pcd = Process(target=clientDeal, args=(newSocket,))
        pcd.start()
    

if __name__ == "__main__":
    main()

```

- 客戶端

```python
from socket import *
from threading import Thread

def sendMsg():
    while True:
        Msg = input("<<")
        if Msg == "end":
            break
        else:
            # 接通之後就不用一直寫ip/port了, 打內容即可
            tcpSocket.send(Msg.encode("utf-8"))
    tcpSocket.close()

def recvMsg():
    while True:
        recvMsg = tcpSocket.recv(1024)
        print("\r>>%s"%recvMsg.decode("utf-8"))
        print("\r<<", end="")


tcpSocket = None

def main():
    global tcpSocket
    tcpSocket = socket(AF_INET, SOCK_STREAM)
    tcpSocket.connect(("192.168.43.114", 8080))
    
    ts = Thread(target=sendMsg)
    tr = Thread(target=recvMsg)
    ts.start()
    tr.start()

if __name__ == "__main__":
    main()
```

網絡通信過程
---
- 網絡號: IP與[網絡掩碼](https://zh.wikipedia.org/wiki/%E5%AD%90%E7%BD%91)(Subnet Mask)[按位與](https://zh.wikipedia.org/wiki/%E4%BD%8D%E6%93%8D%E4%BD%9C)的結果
    - 例如: 192.168.1.1 & 255.255.255.0
    - `11000000.10101000.00000001.00000001`
    - `11111111.11111111.11111111.00000000`
    - `11000000.10101000.00000001.00000000`
    - 192.168.1.0
        - [如何理解子网掩码？](https://www.zhihu.com/question/56895036)
        - [子网掩码有那么难吗？](https://mp.weixin.qq.com/s/jAITB4o1nnO5M2wt0hDqjw)
        - [子网掩码详解](https://blog.csdn.net/jason314/article/details/5447743)
        - [细说IP地址与子网](https://wizardforcel.gitbooks.io/network-basic/content/6.html)
        - [位运算符](https://blog.csdn.net/liubo_01/article/details/80084963)
        - [邏輯（Logical）運算、位元（Bitwise）運算](https://openhome.cc/Gossip/CGossip/LogicalBitwise.html)
        - [IP分類、子網掩碼、網絡號、主機號及ip段](https://kknews.cc/zh-tw/food/mqlob52.html)
    - 而192.168.1.2 & 255.255.255.0的結果亦為192.168.1.0
    - 故192.168.1.1 & 192.168.1.2得以通信
    - 而192.168.11.1&255.255.255.0的結果為192.168.11.0
    - 故無法與192.168.1.1或.1.2通信
    - Q. 假設192.168.1.1與.1.2接上, .1.3 想直接跟電線一樣把網路線擷取電流插上自己的設備, 可否行?
        - 否, 網路線只能有兩頭, 訊號會互相干擾
        - 假設A向B傳送3(00000011), 而C向B傳送4(00000100)
        - 此時B會接收到7(00000111)
        - 此時可以將三台都接上Hub或switch

- [集線器](https://zh.wikipedia.org/wiki/%E9%9B%86%E7%B7%9A%E5%99%A8)(Hub)與[交換機](https://zh.wikipedia.org/wiki/%E7%B6%B2%E8%B7%AF%E4%BA%A4%E6%8F%9B%E5%99%A8)(switch)


    - Q. 兩者差別
        - 集線器每次皆以廣播形式發送, 容易造成堵塞
        - 交換機有智能功能, 首次以廣播功能發送, 爾後便能辨識而使用單播發送
        
- Physical Address aa:bb:cc:dd:ee:ff
    - 此為網卡的序列號(MAC地址)
    - 共有六組兩個16進制數據(6Byte), 
    - 前三組表示廠商地址
    - 後三組表示網卡序號

- 小結:
    - 網卡-MAC
    - 電腦-IP
    - 進程-Port

- Ping的過程(192.168.1.1 Ping .1.2)
  1. ![](https://i.imgur.com/w2bPnpy.png)
  2. ![](https://i.imgur.com/MlBbxDH.png)
  3. ![](https://i.imgur.com/9LpkxvY.png)
  4. ![](https://i.imgur.com/QsFoB9I.png)
  5. ![](https://i.imgur.com/QvHmn0t.png)
  6. ![](https://i.imgur.com/PZImiD8.png)
  7. ![](https://i.imgur.com/k3ZqWEC.png)
  8. ![](https://i.imgur.com/hqXff1R.gif)

    - 流程:
        - ping使用icmp協議
        - 惟 .1.1並不知道 .1.2的ip在哪(圖2)
        - 故先傳arp協議來獲得ip(圖3,4)
    - 注意:
        - ARP傳到switch時是以廣播傳給大家
        - 而取得ip後, switch皆以單播傳送ICMP
    - 知識:
        - osi mode: 對應網絡七層(圖6)
        - MAC地址為[鏈路層](https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C%E6%8E%A5%E5%8F%A3%E5%B1%82%E5%AE%89%E5%85%A8)的一種
        - 鏈路層會收兩個MAC地址
            - FFFF.FFFF.FFFF(廣播MAC地址)
            - 該電腦的網卡序列號
        - 接收後要傳給誰？(圖7,8)
            - Type: 0x806 -> ARP: 根據IP找ARP(圖3,4)
            - Type: 0x800 -> IP(圖5)
        - RARP: 根據MAC找IP
        - ICMP: Ping
        - [ARP攻擊](https://zh.wikipedia.org/wiki/ARP%E6%AC%BA%E9%A8%99)



- [路由器]（(https://zh.wikipedia.org/wiki/%E8%B7%AF%E7%94%B1%E5%99%A8)(Router)
    - 誇網間不得通信
    - 路由器有至少兩張網卡, 其功能為連結兩個以上的網為一個大網
    - [Gateway](https://zh.wikipedia.org/wiki/%E7%BD%91%E5%85%B3)(網關)
        - 假設192.168.1.1要傳給192.168.2.1
        - 惟192.168.1.1無法傳給不能網域
        - 故須經由具有此能力的設備，該設備即稱為網關
    - default gateway
        - 當srcIP與dstIP不在同一區段時, 
        - 電腦會默認將數據轉發給default gateway
    - 一個路由器Ping過程 
    1. ![](https://i.imgur.com/WdM10kC.png)
    2. ![](https://i.imgur.com/Z7xQrGR.png)
    3. ![](https://i.imgur.com/LUdlTZA.png)
    4. ![](https://i.imgur.com/DmF9dI7.png)
    5. ![](https://i.imgur.com/gYgwtrB.png)
    6. ![](https://i.imgur.com/4Vfm7uh.png)
    7. ![](https://i.imgur.com/kWANAMK.png)
    8. ![](https://i.imgur.com/VLxjcFz.png)
    9. ![](https://i.imgur.com/e9fRhVw.png)
    10. ![](https://i.imgur.com/nFFleeh.png)
        - 一開始就是ARP找今晚打老虎的過程
        - 找完之後的第一封(7.)
            - SRC IP: 192.168.1.1
            - SRC ADDR: ABC3
            - DST IP: 192.168.2.1
            - DST ADDR: 9E01
        - 第二封(8.)
            - SRC IP: 192.168.1.1
            - SRC ADDR: 9E02
            - DST IP: 192.168.2.1
            - DST ADDR: 265A
        - 第三封(9.)
            - SRC IP: 192.168.2.1
            - SRC ADDR: 265A
            - DST IP: 192.168.2.1
            - DST ADDR: 9E02
        - 第四封(10.)
            - SRC IP: 192.168.2.1
            - SRC ADDR: 9E01
            - DST IP: 192.168.1.1
            - DST ADDR: ABC3
        - 小結:
            - IP不變, MAC變
            - IP如果變了就不知道要傳給誰了
            - MAC僅用來收發雙方間的約定
            - Q. 為何有了IP標記還需要有MAC?
                - IP僅為邏輯上標記
                - MAC標記實際轉發數據設備地址
                - 假設我要去台北101, 台北101就是DST IP
                - 此時我必須要去搭捷運>客運>捷運
                - 這些行為都是不同的MAC
    - 兩個路由器Ping
    ![](https://i.imgur.com/TpT1xfa.png)
    ![](https://i.imgur.com/CkEVnxu.png)
        - 如路由器A網卡接收的資料DST IP剛好在B網卡所在網段, B會直接發出
        - 如果不是就無法過去
        - 此時設置靜態路由即可解決, 想像成路標即可
    ![](https://i.imgur.com/t8UK9Ad.png)
    - 其他:
        - RIP([路由資訊協定](https://zh.wikipedia.org/wiki/%E8%B7%AF%E7%94%B1%E4%BF%A1%E6%81%AF%E5%8D%8F%E8%AE%AE)): [路由協定](https://zh.wikipedia.org/wiki/%E8%B7%AF%E7%94%B1%E5%8D%8F%E8%AE%AE)的一種, 已經很少人用

- 服務器
    - HTTP server: 
    1. ![](https://i.imgur.com/wvPi0e8.png)
    2. ![](https://i.imgur.com/jjYYCNh.png)
    3. ![](https://i.imgur.com/3qSj0Wv.png)
    4. ![](https://i.imgur.com/zlVgdV0.png)
    5. ![](https://i.imgur.com/45UCnx5.png)
    6. ![](https://i.imgur.com/EkDY2EG.png)
    7. ![](https://i.imgur.com/YrJjPPg.png)
    8. ![](https://i.imgur.com/U0joYYi.png)
    9. ![](https://i.imgur.com/WLgYGH1.png)
    10. ![](https://i.imgur.com/NJEz4KS.png)
    11. ![](https://i.imgur.com/6lVqzgV.png)
        - TCP三次握手
            - client先傳一次SYN跟SERVER請求連接(4)
                - SEQ: 0
                - ACK NUM: 0
                - flags: 0b010010 -> SYN
            - Sever 回傳SYN+ACK答應請求及確認(5)
                - SEQ: 0
                - ACK NUM: 1
                - flags: 0b010010 -> SYN + ACK
                    - ACK: Acknowledgement
            - Client 回傳ACK確認(6)
                - SEQ: 1
                - ACK NUM: 1
                - flags: 0b010000 -> ACK
        - 傳送檔案
            - Client 傳送ACK時, 緊跟著傳HTML在後面(7)
                - SEQ: 1
                - ACK NUM: 1
                - flags: 0b011000 -> ACK+PSH
                    - PSH: PUSH, 趕快送應用層
                - 由於不知道ACK什麼時候送到, 所以就跟著一起送
            - Server 傳送檔案並確認(8)
                - SEQ: 1
                - ACK NUM: 101
                - flags: 0b011000 -> ACK+PSH
        - TCP四次揮手
            - Client 收完檔案後會關閉Socket並傳送FIN(9)
                - SEQ: 101
                - ACK NUM: 122
                - flags: 0b010001 -> ACK+FIN
                    - FIN: finally, 結束連接
                    - 這個ACK是回應上面檔案的, 不算四次揮手
            - Server 知道Client關了後也關閉, 傳FIN(10)
                - SEQ: 122
                - ACK NUM: 102
                - flags: 0b010001 -> ACK+FIN
                    - 這裡有兩個, 只是合成一包
            - Client 回傳ACK確認收到Server關了
                - SEQ: 102
                - ACK NUM: 122
                - flags: 0b010000 -> ACK
        
    - DNS server: 解析域名的協議
        - 域名: 網址 xxx.com, 用來代替不好記的IP
        - 類似電話簿, 域名就是名字, IP就是電話號碼 
        - DNS是UDP協議
        - 查看IP域名: `$ nslookup xxx.com`
        - Q. 為何查詢後有些域名有多個IP?
            - 如果server離client太遠, 那麼訪問的時間就長
            - 為了解決這問題而在多個地方設置Server, 並將相同資料都放一份, 此時client只要訪問最近的server即可
            - 此方法稱為[CDN](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF)
        - 流程:
        ![](https://i.imgur.com/MmVcdFU.png)
        ![](https://i.imgur.com/eIRqyBY.png)
            - 電腦設定好UDP SERVER
            - 發DNS給服務器詢問IP
            - 服務器會回傳IP
            - 接著再跟IP要資料
    - TCP比UDP穩定
        - 因為TCP都有ACK的確認流程
    - TCP 十一種狀態
    ![](https://i.imgur.com/mzbd7ES.png)
        - [TCP的十一种状态及变迁](http://www.rfyy.net/archives/2624.html)
        - [Linux内核TCP/IP参数分析与调优](https://blog.51cto.com/qujunorz/1736665)
        - [TCP Operational Overview and the TCP Finite State Machine (FSM)](http://tcpipguide.com/free/t_TCPOperationalOverviewandtheTCPFiniteStateMachineF-2.htm)
        - MSL(Maximum Segment Lifetime)
            - 數據包於網路上的存活時間
            - Time-Wait到close會等待2MSL的時間
                - 假設最後兩次揮手是
                    - S>fin>C
                    - C>ack>S
                - C>ack>S如何確認有無送達?
                - S如果等待MSL的時間沒有收到ack, S會再次發送fin
                - 亦即如果C等待2MSL都沒收到fin, 那就是送達了
        - TTL(Time to Live)
        ![](https://i.imgur.com/XhCuvYl.png)
            - 數據包在網絡上經過的最大值
            - TTL每經過一次路由器減1
            - 路由器收到TTL為0者會直接丟棄
            - 以此就能推斷數據經過幾個路由器
        - [MSL、TTL及RTT的区别](https://blog.csdn.net/guizaijianchic/article/details/77744004)
    - Q. 輸入www.google.com會有什麼效果?
        - 先解析google對應的ip
            - ARP找Default Gateway的MAC
            - 發DNS協議(查詢IP)給Default Gateway
                - IP: DNS Server
                - DST ADDR: Default gateway
            - 路由器根據路由協議一直轉到DEST Gateway
            - DEST Gateway 用ARP找DNS server的MAC
            - DEST Gateway 把數據傳給DNS server
            - DNS server查詢解析google IP回傳給DNS的Default Gateway, 一直傳回給提出請求的client
        - 取得google的IP後, 發出3次握手取得連接
        - 使用http協議發送請求數據給Web server
        - Web server回傳結果給client
        - client通過瀏覽器顯示網頁
        - 瀏覽器關閉tcp連接(4次揮手)
    - 長連結、短連結
        - 短連結: 
            - 揮手與握手間進行一次讀寫
            - 常用於文字網頁顯示等
            - 減少佔茅坑不拉屎
        - 長連結: 
            - 揮手與握手間拼命讀寫
            - 常用於影片、遊戲
            - 減少揮握手, 提高效率
    - [DHCP](https://zh.wikipedia.org/wiki/%E5%8A%A8%E6%80%81%E4%B8%BB%E6%9C%BA%E8%AE%BE%E7%BD%AE%E5%8D%8F%E8%AE%AE): 找出當前網域誰沒有IP, 分配IP給他

- listen 過程
```python
#Server 
from socket import *
import time

tcpSocket = socket(AF_INET, SOCK_STREAM)
tcpSocket.bind(("", 5566))

# listen 同時最大連接數
numListen = int(input("listen: "))
tcpSocket.listen(numListen)

# 延遲期間, 客戶端同時可以打通listen總數,
# 惟此時還沒accept, 故這些打通的數據會留在一組隊列中, 此時稱為半鏈接
# 亦即收到第一次握手後
for i in range(3):
    print(i)
    time.sleep(1)

# 接著將半鏈接一組一組接通, 數據會移置另一組隊列, 稱為已鏈接
# 亦即收到第三次握手後
while True:
    newSocket, clientInfo = tcpSocket.accept()
    print(clientInfo)
    time.sleep(1)
```
```python
#client
from socket import *

xxxx = int(input("num: "))
for i in range(xxxx):
    tcpSocket = socket(AF_INET, SOCK_STREAM)
    tcpSocket.connect(("127.0.0.1", 5566)) # 127.0.0.1: 本地
    print(i)
```

網路攻擊
--- 
- [SYN flood](https://zh.wikipedia.org/wiki/SYN_flood)
    - [阻斷服務攻擊](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)之一
    - 傳一堆SYN塞爆半鏈接的佇列後不回傳ACK, 讓半鏈接無法鏈接
- [DNS劫持](https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93)
    - 駭客攻擊DNS服務器, 串改域名對應的IP,
    - 引導至釣魚網站
        - 製作介面一模一樣的網站
        - 收集帳密
    - 通常攻擊國家DNS服務器不太容易, 但如果在外面租屋, 房東可能在他路由器設置分配假的DNS IP, 此時解析的就是假的DNS Server
- [DNS欺騙](https://zh.wikipedia.org/wiki/DNS%E9%87%8D%E6%96%B0%E7%BB%91%E5%AE%9A%E6%94%BB%E5%87%BB)
    - 電腦只要收到DNS應答, 即會更新電腦上的IP
    - 而UDP不需要三次握手即可連接, 攻擊者只要不停的傳假IP即可
- [中間人攻擊](https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB)
    - [ARP spoofing](https://zh.wikipedia.org/wiki/ARP%E6%AC%BA%E9%A8%99)的一種
    - A要傳東西給B, 會先傳ARP去要MAC, 
    - 攻擊者只需傳給雙方假的MAC, 攻擊者集會收到雙方的數據
    - 為了不讓雙方知情, 攻擊者會將數據皆傳至正確地方
    - 此時雙方的互通數據皆會流經攻擊者, 一覽無遺
    - 攻擊方法: ARP255
- key: python 原始套接字 

Web服務器
---
Q. 為何我搭建Server後直接用手機訪問卻無法?
- 搭建
    - 電話線>[數據機](https://zh.wikipedia.org/wiki/%E8%B0%83%E5%88%B6%E8%A7%A3%E8%B0%83%E5%99%A8)>路由器>終端
- Key:
    - [LAN](https://zh.wikipedia.org/wiki/%E5%B1%80%E5%9F%9F%E7%BD%91) : 區域網路
    - [WAN](https://zh.wikipedia.org/wiki/%E5%B9%BF%E5%9F%9F%E7%BD%91) : 廣域網路
    - 公有IP: google "I"
    - [私有IP](https://zh.wikipedia.org/wiki/%E4%B8%93%E7%94%A8%E7%BD%91%E7%BB%9C): 無法直接上網
    ![](https://i.imgur.com/bL8zfnB.png)
        - 電腦傳送數據至路由時會轉換成公有IP並記錄
        - Server回數據後, 路由即知要給哪台電腦
        - 故搭建Server直接用手機訪問卻無法, 因為外網數據傳到LAN口時沒有紀錄
        - 解決方法: [埠映射](https://zh.wikipedia.org/wiki/%E7%AB%AF%E5%8F%A3%E6%98%A0%E5%B0%84)

併發服務器
---
- 併行服務器: 當前核數須遠大於當前運行任務數, 不過現行核數通常不大, 故較少人提併行

### Server
```python
from socket import *
from multiprocessing import Process

tcpSocket = socket(AF_INET, SOCK_STREAM)
tcpSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 重複綁定
tcpSocket.bind(("", 5566))
tcpSocket.listen(5)

while True:
    print("-------1---------")
    newSocket, clientInfo = tcpSocket.accept()
    print("-------2---------")
    try:
        while True:
            recvData = newSocket.recv(1024)
            if len(recvData)>0:
                print("%s:%s"%(str(clientInfo), recvData.decode("utf-8")))
            else:
                break
    finally:
        newSocket.close()

tcpSocket.close()
```
### ProcessServer
```python
from socket import *
from multiprocessing import Process

def dealP(newSocket, clientInfo):
    while True:
        recvData = newSocket.recv(1024)
        if len(recvData)>0:
            print("%s:%s"%(str(clientInfo), recvData.decode("utf-8")))
        else:
            break
    newSocket.close()


def main():
    tcpSocket = socket(AF_INET, SOCK_STREAM)
    tcpSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    tcpSocket.bind(("", 5566))
    tcpSocket.listen(5)

    try:
        while True:
            print("---1---")
            newSocket, clientInfo = tcpSocket.accept()
            print("---2---")
            p = Process(target=dealP, args=(newSocket, clientInfo))
            p.start()
            # 子進程已經有一份引用了, 父留著也沒用, 所以就關了吧
            # 但如果是用線程, 那這句就不能關!!!, 因線程是同一個PID運作
            newSocket.close()
    finally:
        tcpSocket.close()
        # 關閉newSocket: 不能收發
        # 關閉tcpSocket.close: 不能接聽

if __name__ == "__main__":
    main()
```

### I/O 多路復用
- 使多個描述符能在一個線程併發
    ###### [聊聊IO多路复用之select、poll、epoll详解](https://www.jianshu.com/p/dfd940e7fca2)
    ###### [I/O多路复用技术（multiplexing）是什么？](https://www.zhihu.com/question/28594409)
    ###### [IO多路复用（IO multiplexing）](https://www.zhihu.com/question/28594409)
    ###### [几种 I/O Multiplexing 方式的比较](http://senlinzhan.github.io/2017/02/17/IO%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/)
    ###### [Redis 和 I/O 多路复用](https://draveness.me/redis-io-multiplexing)

- 非堵塞
```python
from socket import *

tSocket = socket(AF_INET, SOCK_STREAM)
tSocket.bind(("", 5566))
tSocket.listen(5)

while True:
    nSocket, clientInfo = tSocket.accept()
    print("-1-")
  
# 運行後會塞住, 堵塞在tSocket.accept,
# 為了達成看似多任務的效果, 必須讓進程不堵塞而快速進行
```
- setblocking 設定堵塞
    - 預設為True
    - 此案例須將它設為非堵塞(False)
```python
from socket import *

tSocket = socket(AF_INET, SOCK_STREAM)
tSocket.bind(("", 5566))
# 解堵塞
tSocket.setblocking(False)
tSocket.listen(5)

while True:
    nSocket, clientInfo = tSocket.accept()
    print("-1-")
  
# mainProcess走到accept後, accept讀不到client而發生錯誤
```
```$
BlockingIOError: [Errno 35] Resource temporarily unavailable
```
```python
from socket import *
from time import sleep

tSocket = socket(AF_INET, SOCK_STREAM)
tSocket.bind(("", 5566))
tSocket.setblocking(False)
tSocket.listen(5)

while True:
    try:
        nSocket, clientInfo = tSocket.accept()
    except:
        print("-1-")
        sleep(1)
    else:
        print("-2-")
        
# 此時即可正常連接, 沒連接時會不停運行except
# 新問題: 用客戶端連接一個, 上一個連接的就斷了why?
    # 引用計數器
    # 當下一個連接成功後, nSocket指向別人, 原對象為0而回收
    # 解決, 把對象存下來
```
```python
from socket import *
from time import sleep


tSocket = socket(AF_INET, SOCK_STREAM)
tSocket.bind(("", 5566))
tSocket.setblocking(False)
tSocket.listen(5)

# 將用戶信息存下來
clientData = []
while True:
    try:
        nSocket, clientInfo = tSocket.accept()
    except:
        print("-1-")
        sleep(1)
    else:
        print("-2-")
        clientData.append((nSocket, clientInfo))
        nSocket.setblocking(False)


    for nSocket, clientInfo in clientData:
        try:
            # 此時如不輸入也會堵塞, 故解堵後try
            recvData = nSocket.recv(1024)
        except:
            pass
        else:
            if len(recvData)>0:
                print("%s:%s"%(str(clientInfo), recvData.decode("utf-8")))
            else:
                # 結束後沒有關閉nSocket, 導致不停傳送空""
                nSocket.close()
                clientData.remove((nSocket, clientInfo))

# 此過程執行得夠快時, 就有多任務的錯覺效果
# 優點: 省資源
# 缺點: 同時無法處理超大量client
```

- select
    - 同時監視多個描述符的就緒狀況
    - 由os提供的功能, 當某socket能讀或寫時會給通知進程
    - 優點: 幾乎所有平台皆支持
    - 缺點: 效率差
        - 監視數量少
            - Linux: 1024
            - 32位元: 1024
            - 64位元: 2048
        - 使用[輪詢](https://zh.wikipedia.org/wiki/%E8%BC%AA%E8%A9%A2)方式檢測(依序查看哪個socket可讀)
```python
from socket import *
import select
import time

sSocket = socket(AF_INET, SOCK_STREAM)
sSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sSocket.bind(("", 5566))
sSocket.listen(5)
serverList = [sSocket]

while True:
    # 參數1: 檢測socket可否收數據
    # 參數2: 檢測socket可否發數據
    # 參數3: 檢測socket有無異常
    # 如果沒有則堵塞
    readable, writeable, exceptional = select.select(serverList, [], [])
    print(readable)
    print(writeable)
    print(exceptional)

    # 檢測到後解堵塞, return 收到數據的socket給readable
    for sock in readable:
        # 如果是sSocket收到數據, 那就是有電話打進來了> 接聽
        if sock == sSocket:
            nSocket, clientInfo= sSocket.accept()
            serverList.append(nSocket)
        # 如果不是sSocket收到數據, 那就是其中一個nSocket收到數據> 打印
        else:
            recvData = sock.recv(1024)
            if recvData:
                print(recvData.decode("utf-8"))
            else:
                print("offline")
                serverList.remove(sock)
                sock.close()
```
- poll
    - 跟select幾乎一樣, 但解決了socket上限問題
- epoll
    - 沒有1024限制
    - 事件通知機制
```python
import socket
import select

sSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sSocket.bind(("", 5566))
sSocket.listen(5)

# 創建epoll對象
epoll = select.epoll()

# 把socket註冊到epoll裡
epoll.register(sSocket.fileno())

while True:
    # epoll.poll() 相當於 select(read,write,except)
    # 他會返回元祖, 裡面有fd & event兩個值
    epollList = epoll.poll()
    for fd, event in epollList:
        if fd == sSocket.fileno():
            nSocket, clientInfo = sSocket.accept()
            epoll.register(nSocket.fileno())
        elif event == EPOLLIN:
            recvData = xxx.recv(1024)
                # Q.fd沒辦法收數據, 那xxx怎麼寫?
```
```python
import socket
import select

sSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sSocket.bind(("", 5566))
sSocket.listen(5)

epoll = select.epoll()
epoll.register(sSocket.fileno())

# 創兩個空字典
conn = {}
addrInfo = {}

while True:
    epollList = epoll.poll()
    for fd, event in epollList:
        if fd == sSocket.fileno():
            nSocket, clientInfo = sSocket.accept()
            epoll.register(nSocket.fileno())
            
            # 將accept的返回值各自保存為value
            # 假設返回的nSocket是a, fd是4
            # conn = {('4':a)}
            conn[nSocket.fileno()] = nSocket
            addrInfo[nSocket.fileno()] = clientInfo

        elif event == select.EPOLLIN:
            # 此時取出字典裡保存的key即可獲取對應的vaule
            # 例如取conn[4]=a > a.recv
            recvData = conn[fd].recv(1024)
            if recvData>0:
                print(recvData)
            else:
                epoll.unregister(fd)
                conn[fd].close
                print(conn)

# 惟當客戶端連上後所返回得event為4而遲遲無法進入elif,
# 4為EPOLLOUT(可寫)模式
# 且因程序沒處理4而不停被通知, 造成不停解堵塞而無限迴圈
# 解決辦法為在註冊時, 將socket註冊為只檢測可讀
```
```python
import socket
import select

sSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sSocket.bind(("", 5566))
sSocket.listen(5)

epoll = select.epoll()
# 註冊僅可讀
# EPOLLET為邊緣觸發, EPOLL預設為LT(水平觸發)
#     邊緣觸發: 只通知一次
#     水平觸發: 不處理就一直通知
# | 按位或, 相當於添加功能的意思
epoll.register(sSocket.fileno(), select.EPOLLIN | select.EPOLLET)

conn = {}
addrInfo = {}

while True:
    epollList = epoll.poll()
    for fd, event in epollList:
        if fd == sSocket.fileno():
            nSocket, clientInfo = sSocket.accept()
            # 註冊僅可讀, ET
            epoll.register(nSocket.fileno(), select.EPOLLIN | select.EPOLLET)
            
            conn[nSocket.fileno()] = nSocket
            addrInfo[nSocket.fileno()] = clientInfo

        elif event == select.EPOLLIN:
            recvData = conn[fd].recv(1024)       
            if recvData:
                print(recvData)
            else:
                epoll.unregister(fd)
                conn[fd].close
                print("%s>offline"%str(addrInfo[fd]))
```
###### [File Descriptor](https://zh.wikipedia.org/wiki/%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6)、[標準串流](https://zh.wikipedia.org/wiki/%E6%A8%99%E6%BA%96%E4%B8%B2%E6%B5%81)

```python
In [1]: import sys

# 標準串流
In [2]: sys.stdin  # KeyBoard            
Out[2]: <_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>
In [3]: sys.stdout  # Screen          
Out[3]: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>
In [4]: sys.stderr  # Screen          
Out[4]: <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>

# 這個數字就是文件描述符
# 在C語言是數字, 在python為對象
In [5]: sys.stdin.fileno()         
Out[5]: 0
In [6]: sys.stdout.fileno()         
Out[6]: 1
In [7]: sys.stderr.fileno()           
Out[7]: 2

# 每開一個檔案都有一個fd
In [10]: f = open("xxx.txt", "w")
In [11]: f.fileno()                 
Out[11]: 12
```

協程
---
- 協程能減少大量CPU切換, 效率高
- 執行順序可以由開發者自行決定, 進程線程都是os算法決定的
##### CPU Bound: 
- 需要佔用大量CPU
- 適合用多進程(Python線程有限制)
##### I/O Bound: 
- 需要網絡功能, 大量時間都在等待數據
- 適合用線程與協程
```python 
from time import sleep

def test1():
    while True:
        print("-1-")
        yield
        sleep(1)

def test2(x):
    while True:
        print("-2-")
        x.__next__()
        sleep(1)

a = test1()
test2(a)

# CPU僅切換函數裡的變量
# 注意!
    # Python3的generator中next變成了__next__
    # 故如果還寫next()會產生
    # AttributeError: 'generator' object has no attribute 'next'
```
```
-2-
-1-
-2-
-1-
...
```
### greenlet
`$ sudo pip3 install greenlet`
```python
# 需要安裝才能用
from time import sleep
from greenlet import greenlet

def test1():
    while True:
        print("-1-")
        gr2.switch() # 在此停住,切換到2,下次從此開始
        sleep(1)

def test2():
    while True:
        print("-2-")
        gr1.switch() # 在此停住,切換到1,下次從此開始
        sleep(1)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

gr1.switch()
```
```
-1-
-2-
-1-
-2-
...
```

### gevent
- yield跟greenlet都是手動切換,亦即指定在哪時切換
- gevent則是當程序遇到耗時操作時,交出控制權
```python
import gevent

def a(n):
    for i in range(n):
        print(gevent.getcurrent(), i)

g1 = gevent.spawn(a,3)
g2 = gevent.spawn(a,3)
g3 = gevent.spawn(a,3)

g1.join()
g2.join()
g3.join()
# 此程序沒有任何耗時操作, 故一路順暢執行下來
```
```$
<Greenlet at 0x106282488: a(3)> 0
<Greenlet at 0x106282488: a(3)> 1
<Greenlet at 0x106282488: a(3)> 2
<Greenlet at 0x106282598: a(3)> 0
<Greenlet at 0x106282598: a(3)> 1
<Greenlet at 0x106282598: a(3)> 2
<Greenlet at 0x1062826a8: a(3)> 0
<Greenlet at 0x1062826a8: a(3)> 1
<Greenlet at 0x1062826a8: a(3)> 2
```
```python
import gevent

def a(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        # 當gevent遇到耗時, 交出控制權
        # 注意!不是timeModule
        # gevent把常用的耗時操作都重寫一遍(ex.recv, time, accept)
        gevent.sleep(1)

g1 = gevent.spawn(a,3)
g2 = gevent.spawn(a,3)
g3 = gevent.spawn(a,3)

g1.join()
g2.join()
g3.join()
```
```$
<Greenlet at 0x10d518488: a(3)> 0
<Greenlet at 0x10d518598: a(3)> 0
<Greenlet at 0x10d5186a8: a(3)> 0
<Greenlet at 0x10d518488: a(3)> 1
<Greenlet at 0x10d518598: a(3)> 1
<Greenlet at 0x10d5186a8: a(3)> 1
<Greenlet at 0x10d518488: a(3)> 2
<Greenlet at 0x10d518598: a(3)> 2
<Greenlet at 0x10d5186a8: a(3)> 2
```

