Num01-->TCP通信模型
Test01-->TCP客戶端案例
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
from socket import *
def main():
# 1.創(chuàng)建socket
client_socket = socket(AF_INET, SOCK_STREAM)
# 2.指定服務器的地址和端口號
server_addr = ('192.168.105.125',8080)
client_socket.connect(server_addr)
print('connect %s success' % str(server_addr))
while True:
# 3.給用戶提示,讓用戶輸入要檢索的資料
send_data = input('>>')
# 退出
if send_data == 'quit':
break
# 向服務器請求數據
client_socket.send(send_data.encode())
client_socket.close()
if __name__ == "__main__":
main()
Test02-->TCP服務器端案例
TCP服務器端創(chuàng)建流程如下:
1,socket創(chuàng)建一個套接字
2,bind綁定ip和port
3,listen使套接字變?yōu)榭梢员粍渔溄?br> 4,accept等待客戶端的鏈接
5,recv/send接收/發(fā)送數據
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
from socket import *
import time
def main():
# 1.創(chuàng)建socket,stream流式套接字,對應tcp
listen_socket = socket(AF_INET, SOCK_STREAM)
# 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
# 設置socket層屬性 復用地址 允許
listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 2.綁定端口號
my_addr = ('192.168.105.125', 8080)
#shift + insert
listen_socket.bind(my_addr)
# 3.接聽狀態(tài)
#listen中的black表示已經建立鏈接和半鏈接的總數
#如果當前已建立鏈接數和半鏈接數以達到設定值,那么新客戶端就不會connect成功,而是等待服務器。直到有鏈接退出。
listen_socket.listen(4)
print('listening...')
# 4.等待客戶端來請求服務器
while True:
# 接受連接請求,創(chuàng)建新的連接套接字,用于客戶端連通信
connect_socket, client_addr = listen_socket.accept()
# accept默認會引起阻塞
# 新創(chuàng)建連接用的socket, 客戶端的地址
# print(connect_socket)
# print(client_addr)
while True:
# tcp recv() 只會返回接收到的數據
# 1024表示接受的數據長度
recv_data = connect_socket.recv(1024)
if len(recv_data) == 0:
#發(fā)送方關閉tcp的連接,recv()不會阻塞,而是直接返回''
print('client %s close' % str(client_addr))
time.sleep(5)
break
print('recv: %s' % recv_data.decode('gbk'))
# 用完之后,關閉新創(chuàng)建的那個connect_socket
connect_socket.close()
if __name__ == "__main__":
main()
Num02-->TCP協(xié)議三次握手
Num03-->TCP協(xié)議四次揮手
Num04-->TCP協(xié)議十種狀態(tài)
當一端收到一個FIN,內核讓read返回0來通知應用層另一端已經終止了向本端的數據傳送
發(fā)送FIN通常是應用層對socket進行關閉的結果
Num05-->TCP協(xié)議的2MSL問題
加以說明:
1,2MSL即兩倍的MSL,TCP的TIME_WAIT狀態(tài)也稱為2MSL等待狀態(tài)。
2,當TCP的一端發(fā)起主動關閉,在發(fā)出最后一個ACK包后,
3,即第3次握 手完成后發(fā)送了第四次握手的ACK包后就進入了TIME_WAIT狀態(tài),
4,必須在此狀態(tài)上停留兩倍的MSL時間,
5,等待2MSL時間主要目的是怕最后一個 ACK包對方沒收到,
6,那么對方在超時后將重發(fā)第三次握手的FIN包,
7,主動關閉端接到重發(fā)的FIN包后可以再發(fā)一個ACK應答包。
8,在TIME_WAIT狀態(tài) 時兩端的端口不能使用,要等到2MSL時間結束才可繼續(xù)使用。
9,當連接處于2MSL等待階段時任何遲到的報文段都將被丟棄。
10,不過在實際應用中可以通過設置 SO_REUSEADDR選項達到不必等待2MSL時間結束再使用此端口。
Num06-->TCP協(xié)議長鏈接和短鏈接
TCP在真正的讀寫操作之前,server與client之間必須建立一個連接,
當讀寫操作完成后,雙方不再需要這個連接時它們可以釋放這個連接,
連接的建立通過三次握手,釋放則需要四次握手,
所以說每個連接的建立都是需要資源消耗和時間消耗的。
Test01-->長鏈接
1, client 向 server 發(fā)起連接
2,server 接到請求,雙方建立連接
3,client 向 server 發(fā)送消息
4,server 回應 client
5,一次讀寫完成,連接不關閉
6,后續(xù)讀寫操作...
7,長時間操作之后client發(fā)起關閉請求
Test02-->短鏈接
1,client 向 server 發(fā)起連接請求
2,server 接到請求,雙方建立連接
3,client 向 server 發(fā)送消息
4,server 回應 client
5,一次讀寫完成,此時雙方任何一個都可以發(fā)起 close 操作
Test03-->長鏈接和短鏈接的區(qū)別
長鏈接可以省去較多的TCP建立和關閉的操作,減少浪費,節(jié)約時間。
對于頻繁請求資源的客戶來說,較適用長連接。
client與server之間的連接如果一直不關閉的話,會存在一個問題,
隨著客戶端連接越來越多,server早晚有扛不住的時候,這時候server端需要采取一些策略,
如關閉一些長時間沒有讀寫事件發(fā)生的連接,這樣可以避免一些惡意連接導致server端服務受損;
如果條件再允許就可以以客戶端機器為顆粒度,限制每個客戶端的最大長連接數,這樣可以完全避免某個蛋疼的客戶端連累后端服務。
短鏈接對于服務器來說管理較為簡單,存在的連接都是有用的連接,不需要額外的控制手段。但如果客戶請求頻繁,將在TCP的建立和關閉操作上浪費時間和帶寬。
Test04-->TCP長/短鏈接的應用場景
長鏈接多用于操作頻繁,點對點的通訊,而且連接數不能太多情況。
每個TCP連接都需要三次握手,這需要時間,如果每個操作都是先連接,
再操作的話那么處理速度會降低很多,所以每個操作完后都不斷開,
再次處理時直接發(fā)送數據包就OK了,不用建立TCP連接。
例如:數據庫的連接用長連接,如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創(chuàng)建也是對資源的浪費。
像WEB網站的HTTP服務一般都用短鏈接,因為長連接對于服務端來說會耗費一定的資源。
像WEB網站這么頻繁的成千上萬甚至上億客戶端的連接,用短連接會更省一些資源;如果用長連接,而且同時有成千上萬的用戶,如果每個用戶都占用一個連接的話,那可想而知吧。雖然并發(fā)量大,但每個用戶無需頻繁操作情況下需用短連好。
Num07-->TCP并發(fā)服務器--多進程實現
通過為每個客戶端創(chuàng)建一個進程的方式,能夠同時為多個客戶端進行服務。當客戶端不是特別多的時候,這種方式還行,如果有幾百上千個,就不可取了,因為每次創(chuàng)建進程等過程需要好較大的資源。
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author : xiaoke
from multiprocessing import Process
from socket import *
# 需要為客戶端提供服務
def do_service(connect_socket):
while True:
recv_data = connect_socket.recv(1024)
if len(recv_data) == 0:
# 發(fā)送方關閉tcp的連接,recv()不會阻塞,而是直接返回''
# print('client %s close' % str(client_addr))
# s.getpeername() s.getsockname()
print('client %s close' % str(connect_socket.getpeername()))
break
print('recv: %s' % recv_data.decode('gbk'))
def main():
# 1.創(chuàng)建socket
listen_socket = socket(AF_INET, SOCK_STREAM)
# stream流式套接字,對應tcp
# 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
# 設置socket層屬性 復用地址,不用等2msl, 允許
listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 2.綁定端口
my_addr = ('192.168.105.125', 8080)
listen_socket.bind(my_addr)
# 3,接聽狀態(tài)
listen_socket.listen(4) # 設置套接字成監(jiān)聽,4表示一個己連接隊列長度
print('listening...')
# 4.等待客戶端來請求
# 父進程只專注接受連接請求
while True:
# 接受連接請求,創(chuàng)建連接套接字,用于客戶端間通信
connect_socket, client_addr = listen_socket.accept() # accept默認會引起阻塞
# 新創(chuàng)建連接用的socket, 客戶端的地址
# print(connect_socket)
print(client_addr)
# 每當來新的客戶端連接,創(chuàng)建子進程,由子進程和客戶端通信
process_do_service = Process(target=do_service, args=(connect_socket,))
process_do_service.start()
# 父進程,關閉connect_socket
connect_socket.close()
if __name__ == "__main__":
main()
Num08-->TCP并發(fā)服務器--多線程實現
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author : xiaoke
from socket import *
from threading import Thread
# 需要為客戶端提供服務
def do_service(connect_socket):
while True:
recv_data = connect_socket.recv(1024)
if len(recv_data) == 0:
# 發(fā)送方關閉tcp的連接,recv()不會阻塞,而是直接返回''
# print('client %s close' % str(client_addr))
# s.getpeername() s.getsockname()
print('client %s close' % str(connect_socket.getpeername()))
break
print('recv: %s' % recv_data.decode('gbk'))
def main():
# 1.創(chuàng)建socket
listen_socket = socket(AF_INET, SOCK_STREAM)
# 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
# 設置socket層屬性 復用地址,不用等2msl 允許
listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 2.綁定端口
my_addr = ('192.168.105.125', 8080)
# shift + insert
listen_socket.bind(my_addr)
# 3.接聽狀態(tài)
listen_socket.listen(4) # 設置套接字成監(jiān)聽,4表示一個己連接隊列長度
print('listening...')
# 4.等待來電話
# 主線程只專注接受連接請求
while True:
# 接受連接請求,創(chuàng)建連接套接字,用于客戶端連通信
connect_socket, client_addr = listen_socket.accept() # accept默認會引起阻塞
# 新創(chuàng)建連接用的socket, 客戶端的地址
# print(connect_socket)
print(client_addr)
# 每當來新的客戶端連接,創(chuàng)建子線程,由子線程和客戶端通信
thread_do_service = Thread(target=do_service, args=(connect_socket,))
thread_do_service.start()
# 主線程,不能關閉connect_socket,多個線程共享打開的文件
# connect_socket.close()
if __name__ == "__main__":
main()
Num09-->TCP單進程阻塞服務器實現
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author : xiaoke
import time
from socket import *
def main():
# 1.創(chuàng)建socket
listen_socket = socket(AF_INET, SOCK_STREAM)
# 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
# 設置socket層屬性 復用地址 允許
listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 2.綁定端口
my_addr = ('192.168.105.125', 8080)
listen_socket.bind(my_addr)
# 3.接聽狀態(tài)
listen_socket.listen(4)
print('listening...')
# 4.等待客戶端發(fā)起請求
while True:
# 接受連接請求,創(chuàng)建連接套接字,用于客戶端間通信
connect_socket, client_addr = listen_socket.accept() # accept默認會引起阻塞
# 新創(chuàng)建連接用的socket, 客戶端的地址
# print(connect_socket)
print(client_addr)
while True:
# tcp recv() 只會返回接收到的數據
recv_data = connect_socket.recv(1024)
if len(recv_data) == 0:
# 發(fā)送方關閉tcp的連接,recv()不會阻塞,而是直接返回''
print('client %s close' % str(client_addr))
time.sleep(5)
break
print('recv: %s' % recv_data.decode('gbk'))
# 用完之后,關閉connect_socket
connect_socket.close()
if __name__ == "__main__":
main()
Num10-->TCP單進程非阻塞服務器實現
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author : xiaoke
import time
from socket import *
def main():
# 1.創(chuàng)建socket
listen_socket = socket(AF_INET, SOCK_STREAM)
# 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
# 設置socket層屬性 復用地址 允許
listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 設置listen_socket為非阻塞方式
listen_socket.setblocking(False) # 設置非阻塞
# 2.綁定端口
my_addr = ('192.168.105.125', 8080)
listen_socket.bind(my_addr)
# 3.接聽狀態(tài)
listen_socket.listen(4)
print('listening...')
# 4.等待客戶端發(fā)起請求
# 創(chuàng)建一個列表,保存已連接socket
connect_socket_list = []
while True:
print(connect_socket_list)
try:
# 接受連接請求,創(chuàng)建連接套接字,用于客戶端連通信
connect_socket, client_addr = listen_socket.accept() # accept默認會引起阻塞
except Exception as e: # 還沒有客戶端連接
# print(e)
# time.sleep(1)
pass
else: # 此時有連接請求
# 新創(chuàng)建連接用的socket, 客戶端的地址
print('有新的客戶端連接 %s' % str(client_addr))
# 將新socket 設成非阻塞
connect_socket.setblocking(False)
# 將新的socket添加到列表中,以便后續(xù)循環(huán)讀數據
connect_socket_list.append(connect_socket)
# 保存刪除socket列表
need_delete_socket_list = []
# 遍歷已連接的socket分別讀數據
for new_socket in connect_socket_list:
try:
recv_data = new_socket.recv(1024)
except:
pass
else:
# 如果對方關閉
if len(recv_data) == 0:
print('%s close' % (str(new_socket.getpeername())))
new_socket.close()
# 從connect_socket_list列表中刪除,單獨使用列表保存要刪除socket
need_delete_socket_list.append(new_socket)
continue
print('from %s : %s' %
(str(new_socket.getpeername()), recv_data.decode('gbk')))
# 從connect_socket_list刪除已關閉soccket
for s in need_delete_socket_list:
connect_socket_list.remove(s)
time.sleep(1)
if __name__ == "__main__":
main()
Num11-->select版--TCP服務器實現
Test01-->select 原理
在多路復用的模型中,比較常用的有select模型和epoll模型。這兩個都是系統(tǒng)接口,由操作系統(tǒng)提供。當然,Python的select模塊進行了更高級的封裝。
網絡通信被Unix系統(tǒng)抽象為文件的讀寫,通常是一個設備,由設備驅動程序提供,驅動可以知道自身的數據是否可用。支持阻塞操作的設備驅動通常會實現一組自身的等待隊列,如讀/寫等待隊列用于支持上層(用戶層)所需的block或non-block操作。設備的文件的資源如果可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到數據到來可用的時候,再喚醒進程。
這些設備的文件描述符被放在一個數組中,然后select調用的時候遍歷這個數組,如果對于文件描述符可讀則會返回該文件描述符。當遍歷結束之后,如果仍然沒有一個可用設備文件描述符,select則讓用戶進程睡眠,直到等待資源可用的時候再喚醒,喚醒之后遍歷之前那個監(jiān)視的數組。每次遍歷都是依次進行判斷的。
Test02-->select的優(yōu)缺點
優(yōu)點:select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優(yōu)點。
缺點:select的一個缺點在于單個進程能夠監(jiān)視的文件描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。
一般來說這個數目和系統(tǒng)內存關系很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
對socket進行掃描時是依次掃描的,即采用輪詢的方法,效率較低。
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。
Test03-->案例的實現代碼
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author : xiaoke
import select
import sys # sys.stdin 代表鍵盤設備的文件對象
from socket import *
def main():
# 1.創(chuàng)建socket
listen_socket = socket(AF_INET, SOCK_STREAM)
# 設置允許復用地址,當建立連接之后服務器先關閉,設置地址復用
# 設置socket層屬性 復用地址 允許
listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 2.綁定端口
my_addr = ('192.168.105.125', 8080)
listen_socket.bind(my_addr)
# 3.接聽狀態(tài)
listen_socket.listen(4)
print('listening...')
# 指定select關心的哪些路(socket,或文件)數據
rlist = [listen_socket, sys.stdin] # 要讀的文件對象列表,包括listen socket
wlist = [] # 要寫的文件對象列表
xlist = [] # 出現異常的文件對象列表
while True:
print(rlist)
# select 會阻塞等待三個列表中文件對象就緒,如果沒就緒,select一直阻塞;只有任意文件就緒,select返回
# 就緒的文件列表
read_ready_list, wready, excplist = select.select(rlist, wlist, xlist)
# 指定seclet關注讀,寫,異常文件列表
# 如果select返回,一定有客戶端連接服務器
# 循環(huán)判斷是哪個關注的文件,讀就緒了
for fobj in read_ready_list:
# 如果fobj是listen_socket對象,一定有客戶端連接服務器
if fobj == listen_socket:
new_socket, peer_addr = fobj.accept() # 此時accept調用一定不會阻塞
print(peer_addr)
# 將新的socket添加至rlist,也要進行關注
rlist.append(new_socket)
elif fobj == sys.stdin: # 鍵盤有數據輸入
data = sys.stdin.readline() # input()
print('input %s' % data)
if data == 'quit\n':
exit()
else: # 已連接socket有數據可讀
recv_data = fobj.recv(1024)
if len(recv_data) > 0:
print('from %s : %s' % (str(fobj.getpeername()), recv_data.decode('gbk')))
else: # 客戶端關閉socket
print('%s close' % str(fobj.getpeername()))
fobj.close() # 將關閉socket從rlist列表中刪除,表示不再關注這個socket
rlist.remove(fobj)
if __name__ == "__main__":
main()
Num12-->epoll版--TCP服務器實現
Test01-->epoll的優(yōu)點:
沒有最大并發(fā)連接的限制,能打開的FD(指的是文件描述符,通俗的理解就是套接字對應的數字編號)的上限遠大于1024
效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;即epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環(huán)境中,epoll的效率就會遠遠高于select和poll。
Test02-->一些術語
EPOLLIN (可讀)
EPOLLOUT (可寫)
EPOLLET (ET模式)
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。
LT模式是默認模式,LT模式與ET模式的區(qū)別如下:
LT模式:當epoll檢測到描述符事件發(fā)生并將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll時,會再次響應應用程序并通知此事件。
ET模式:當epoll檢測到描述符事件發(fā)生并將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll時,不會再次響應應用程序并通知此事件。
Test03-->案例的實現代碼
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author : xiaoke
import socket
import select
# 創(chuàng)建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 設置可以重復使用綁定的信息
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定本機信息
s.bind(("", 8080))
# 變?yōu)楸粍?s.listen(10)
# 創(chuàng)建一個epoll對象
epoll = select.epoll()
# 測試,用來打印套接字對應的文件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET
# 注冊事件到epoll中
# epoll.register(fd[, eventmask])
# 注意,如果fd已經注冊過,則會發(fā)生異常
# 將創(chuàng)建的套接字添加到epoll的事件監(jiān)聽中
epoll.register(s.fileno(), select.EPOLLIN | select.EPOLLET)
connections = {}
addresses = {}
# 循環(huán)等待客戶端的到來或者對方發(fā)送數據
while True:
# epoll 進行 fd 掃描的地方 -- 未指定超時時間則為阻塞等待
epoll_list = epoll.poll()
# 對事件進行判斷
for fd, events in epoll_list:
# print fd
# print events
# 如果是socket創(chuàng)建的套接字被激活
if fd == s.fileno():
conn, addr = s.accept()
print('有新的客戶端到來%s' % str(addr))
# 將 conn 和 addr 信息分別保存起來
connections[conn.fileno()] = conn
addresses[conn.fileno()] = addr
# 向 epoll 中注冊 連接 socket 的 可讀 事件
epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
elif events == select.EPOLLIN:
# 從激活 fd 上接收
recvData = connections[fd].recv(1024)
if len(recvData) > 0:
print('recv:%s' % recvData)
else:
# 從 epoll 中移除該 連接 fd
epoll.unregister(fd)
# server 側主動關閉該 連接 fd
connections[fd].close()
print("%s---offline---" % str(addresses[fd]))