網路編程 === - [海底光纜](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 ```