網路編程
===
- [海底光纜](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協議
- 
- SendReadRequest要傳給69Port,
- 接著服務器回傳是以新的隨機Port回傳,
- 後面的資料傳輸皆是傳向新Port而非69!!
- 
- 如何確定傳送已完成?
- 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. 
2. 
3. 
4. 
5. 
6. 
7. 
8. 
- 流程:
- 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. 
2. 
3. 
4. 
5. 
6. 
7. 
8. 
9. 
10. 
- 一開始就是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


- 如路由器A網卡接收的資料DST IP剛好在B網卡所在網段, B會直接發出
- 如果不是就無法過去
- 此時設置靜態路由即可解決, 想像成路標即可

- 其他:
- 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. 
2. 
3. 
4. 
5. 
6. 
7. 
8. 
9. 
10. 
11. 
- 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)
- 流程:


- 電腦設定好UDP SERVER
- 發DNS給服務器詢問IP
- 服務器會回傳IP
- 接著再跟IP要資料
- TCP比UDP穩定
- 因為TCP都有ACK的確認流程
- TCP 十一種狀態

- [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)

- 數據包在網絡上經過的最大值
- 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): 無法直接上網

- 電腦傳送數據至路由時會轉換成公有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
```