socket编程listen函数限制连接数的解决方案_网络编程——服务器篇
目錄
一、客戶端實現
二、單進程服務器
2.1 單進程實現
2.2 單進程非阻塞實現
2.3 TCP服務器(select版)
2.4 epoll版服務器實現
三、多進程服務器和多線程服務器
四、協程
4.1 協程的生成器實現
4.2 協程的greenlet實現
4.3 協程的gevent實現
4.3.1 gevent的使用
4.3.2 gevent的切換執行
4.3.3 gevent的服務器實現
一、客戶端實現
客戶端比較簡單,并且適用于與不同服務器通信,代碼如下:
#coding=utf-8from socket import *
import random
import time
serverIp = raw_input("請輸?服務器的ip:")
connNum = raw_input("請輸?要鏈接服務器的次數(例如1000):")
g_socketList = []
for i in range(int(connNum)):
s = socket(AF_INET, SOCK_STREAM)
s.connect((serverIp, 7788))
g_socketList.append(s)
print(i)
while True:
for s in g_socketList:
s.send(str(random.randint(0,100)))
# ?來測試?
#time.sleep(1)
二、單進程服務器
2.1 單進程實現
Linux服務器開發學習視頻資料,包括Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等,需要知識技術學習視頻文檔資料的朋友可以加我群720209036獲取
單進程完成一個tcp服務器,同時只能為一個客戶端服務。
from socket import *serSocket = socket(AF_INET, SOCK_STREAM)
# 重復使?綁定的信息,若服務器先close,則不用等待2MSL,可以直接綁定下一個客戶端
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
while True:
print('-----主進程, , 等待新客戶端的到來------')
newSocket,destAddr = serSocket.accept()
print('-----主進程, , 接下來負責數據處理[%s]-----'%str(destAddr))
try:
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客戶端已經關閉'%str(destAddr))
break
finally:
newSocket.close()
serSocket.close()
2.2 單進程非阻塞實現
上面單進程實現同時只能為一個服務端服務,如果第二個while中不阻塞,則可以實現多用戶同時服務。代碼如下:
#coding=utf-8from socket import *
import time
# ?來存儲所有的新鏈接的socket
g_socketList = []
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
#可以適當修改listen中的值來看看不同的現象
serSocket.listen(1000)
#將套接字設置為?堵塞
#設置為?堵塞后, 如果accept時, 恰巧沒有客戶端connect, 那么accept會
#產??個異常, 所以需要try來進?處理
serSocket.setblocking(False)
while True:
#?來測試
#time.sleep(0.5)
try:
newClientInfo = serSocket.accept()
except Exception as result:
pass
else:
print("?個新的客戶端到來:%s"%str(newClientInfo))
newClientInfo[0].setblocking(False)
g_socketList.append(newClientInfo)
# ?來存儲需要刪除的客戶端信息
needDelClientInfoList = []
# 為列表中每個客戶端服務
for clientSocket,clientAddr in g_socketList:
try:
recvData = clientSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(clientAddr), recvData))
else:
print('[%s]客戶端已經關閉'%str(clientAddr))
clientSocket.close()
g_needDelClientInfoList.append((clientSocket,clientAddr))
except Exception as result:
pass
for needDelClientInfo in needDelClientInfoList:
g_socketList.remove(needDelClientInfo)
if __name__ == '__main__':
main()
2.3 TCP服務器(select版)
tcp/ip學習文檔代碼資料加我群720209036獲取
在非阻塞版本中使用for循環為列表中的每個客戶端服務,而select版是通過調用select函數直接返回列表中接收到數據的socket,不必循環遍歷。
優點:幾乎所有平臺都支持,有良好的跨平臺性。
缺點:select的?個缺點在于單個進程能夠監視的?件描述符的數量存在最?限制,在Linux上?般為1024, 可以通過修改宏定義甚?重新編譯內核的?式提升這?限制, 但是這樣也會造成效率的降低。
?般來說這個數? 和系統內存關系很?, 具體數? 可以cat /proc/sys/fs/filemax查看。 32位機默認是1024個。 64位機默認是2048.個。對socket進?掃描時是依次掃描的, 即采?輪詢的?法, 效率較低。
當套接字?較多的時候, 每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度, 不管哪個Socket是活躍的, 都遍歷?遍。 這會浪費很多CPU時間。
select函數解釋如圖:
select版tcp服務器代碼如下:
import selectimport socket
import sys
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 7788))
server.listen(5)
inputs = [server, sys.stdin]
running = True
while True:
# 調用 select 函數,阻塞等待
readable, writeable, exceptional = select.select(inputs, [], [])
# 數據抵達,循環
for sock in readable:
# 監聽到有新的連接
if sock == server:
conn, addr = server.accept()
# select 監聽的socket
inputs.append(conn)
# 監聽到鍵盤有輸入
elif sock == sys.stdin:
cmd = sys.stdin.readline()
running = False
break
# 有數據到達
else:
# 讀取客戶端連接發送的數據
data = sock.recv(1024)
if data:
sock.send(data)
else:
# 移除select監聽的socket
inputs.remove(sock)
sock.close()
# 如果檢測到用戶輸入敲擊鍵盤,那么就退出
if not running:
break
server.close()
2.4 epoll版服務器實現
為了解決select版并發連接數目的限制,出現了poll版,與select版幾乎相同,唯一不同的是數量不受限制,仍是用的輪詢方式。后來為了解決poll版輪詢監測方式低下的問題出現了epoll版,epoll版相當于“有問題舉手”,而不是“挨個問是否有問題”。
epoll版的優點:
1. 沒有最?并發連接的限制, 能打開的FD(指的是?件描述符), 通俗的理解就是套接字對應的數字編號)的上限遠?于1024
2. 效率提升, 不是輪詢的?式, 不會隨著FD數?的增加效率下降。 只有活躍可?的FD才會調?callback函數; 即epoll最?的優點就在于它只管你“活躍”的連接, ?跟連接總數?關, 因此在實際的?絡環境中, epoll的效率就會遠遠?于select和poll。
epoll版tcp服務器代碼如下:
代碼解釋:
epoll的三種事件:
EPOLLIN (可讀)
EPOLLOUT (可寫)
EPOLLET (ET模式)
epoll對?件描述符的操作有兩種模式: LT(level trigger 水平觸發) 和ET(edge trigger 邊沿觸發) 。 LT模式是默認模式, LT模式與ET模式的區別如下:
LT模式: 當epoll檢測到描述符事件發?并將此事件通知應?程序, 應?程序可以不?即處理該事件
ET模式: 當epoll檢測到描述符事件發?并將此事件通知應?程序, 應?程序必須?即處理該事件,否則會丟失
import socketimport select
# 創建套接字
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 設置可以重復使?綁定的信息
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 綁定本機信息
s.bind(("",7788))
# 變為被動
s.listen(10)
# 創建?個epoll對象
epoll=select.epoll()
# 測試, ?來打印套接字對應的?件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET
# 注冊事件到epoll中
# epoll.register(fd, [eventmask])
# 注意, 如果fd已經注冊過, 則會發?異常
# 將創建的socket添加到epoll的事件監聽中
# [eventmask]為監聽的事件列表,有列表中的事件時才會放入epoll列表,
# 事件有三種,EPOLLIN接收數據事件,EPOLLOUT發送數據,EPOLLET模式(水平觸發或邊沿觸發)
epoll.register(s.fileno(),select.EPOLLIN|select.EPOLLET)
# connections用于存儲socket,addresses用于存儲端口,
# 它們都為字典,key為socket的文件描述符,value為socket或端口!
connections = {}
addresses = {}
# 循環等待客戶端的到來或者對?發送數據
while True:
# epoll 進? fd 掃描的地? -- 未指定超時時間則為阻塞等待
# 等價于select版本中的 readable,xxx,yyy = select([],[],[])
# 不為輪詢,使用的是事件通知機制,為本代碼的核心
epoll_list=epoll.poll()
# 對事件進?判斷
for fd,events in epoll_list:
# print fd
# print events
# 如果是socket創建的套接字被激活
if fd == s.fileno():
conn,addr=s.accept()
print('有新的客戶端到來%s'%str(addr))
# 將 conn 和 addr 信息分別保存起來
# 注意connections和address為字典,以key、value存儲
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]))
三、多進程服務器和多線程服務器
代碼說明:
1、多進程實現和多線程實現幾乎相同,不同點:1、創建的時候;2、while中多進程實現中,由于子進程復制了一份,所以可以關閉,多線程中,子線程之間共享資源,所以在while中不能關閉。
2、代碼中使用try...finally,目的是可以使用Ctrl+C強制結束進程或線程。
多進程服務器代碼如下:
#coding=utf-8from socket import *
from multiprocessing import *
from time import sleep
# 處理客戶端的請求并為其服務
def dealWithClient(newSocket,destAddr):
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客戶端已經關閉'%str(destAddr))
break
newSocket.close()
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
try:
while True:
print('-----主進程,,等待新客戶端的到來------')
newSocket,destAddr = serSocket.accept()
print('-----主進程,,接下來創建?個新的進程負責數據處理------')
client = Process(target=dealWithClient, args=(newSocket,destAddr))
client.start()
#因為已經向?進程中copy了?份(引?),并且?進程中這個套接字所以關閉
newSocket.close()
finally:
#當為所有的客戶端服務完之后再進?關閉,表示不再接收新的客戶端的鏈接
serSocket.close()
if __name__ == '__main__':
main()
多線程服務器代碼如下:
#coding=utf-8from socket import *
from threading import Thread
from time import sleep
# 處理客戶端的請求并執?事情
def dealWithClient(newSocket,destAddr):
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客戶端已經關閉'%str(destAddr))
break
newSocket.close()
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
try:
while True:
print('-----主進程, , 等待新客戶端的到來------')
newSocket,destAddr = serSocket.accept()
print('-----主進程, , 接下來創建?個新的進程負責數據處理[%s]-----')
client = Thread(target=dealWithClient, args=(newSocket,destAddr))
client.start()
#因為線程中共享這個套接字, 如果關閉了會導致這個套接字不可?,
#但是此時在線程中這個套接字可能還在收數據, 因此不能關閉
#newSocket.close()
finally:
serSocket.close()
if __name__ == '__main__':
main()
四、協程
協程文檔代碼學習資料加我群720209036獲取
進程里面有線程,線程里面有協程。協程不牽扯到切換,并且能完成多任務。
注意:計算密集型時用多進程;IO密集型使用多線程、多協程。
通俗的理解: 在?個線程中的某個函數, 可以在任何地?保存當前函數的?些臨時變量等信息, 然后切換到另外?個函數中執?, 注意不是通過調?函數的?式做到的, 并且切換的次數以及什么時候再切換到原來的函數都由開發者??確定。
協程和線程的區別:線程切換從系統層?遠不?保存和恢復 CPU上下?這么簡單。 操作系統為了程序運營的?效性每個線程都有??緩存Cache等等數據, 操作系統還會幫你做這些數據的恢復操作。所以線程的切換?常耗性能。 但是協程的切換只是單純的操作CPU的上下?, 所以?秒鐘切換個上百萬次系統都抗的住。
協程調度:操作系統不感知協程,所以操作系統不會對協程調度。 ?前的協程框架?般都是設計成 1:N 模式。 所謂 1:N 就是?個線程作為?個容器??放置多個協程。 那么誰來適時的切換這些協程? 答案是有協程自己主動讓出CPU, 也就是每個協程池??有?個調度器, 這個調度器是被動調度的。 意思就是他不會主動調度。 ?且當?個協程發現自己執行不下去了(比如異步等待?絡的數據回來, 但是當前還沒有數據到), 這個時候就可以由這個協程通知調度器, 這個時候執?到調度器的代碼, 調度器根據事先設計好的調度算法找到當前最需要CPU的協程。 切換這個協程的CPU上下?把CPU的運?權交給這個協程, 直到這個協程出現執行不下去需要等等的情況, 或
者它調?主動讓出CPU的API之類, 觸發下?次調度。
協程調度存在問題:假設一個線程中有?個協程是CPU密集型的他沒有IO操作, 也就是自己不會主動觸發調度器調度的過程, 那么就會出現其他協程得不到執?的情況, 所以這種情況下需要程序員? ?避免。 這是?個問題, 假設業務開發的?員并不懂這個原理的話就可能會出現問題。
協程的優點:在IO密集型的程序中由于IO操作遠遠慢于CPU的操作, 所以往往需要CPU去等IO操作。 同步IO下系統需要切換線程, 讓操作系統可以在IO過程中執?其他的東?。 這樣雖然代碼是符合?類的思維習慣但是由于?量的線程切換帶來了?量的性能的浪費, 尤其是IO密集型的程序。
所以?們發明了異步IO。 就是當數據到達的時候觸發我的回調。 來減少線程切換帶來性能損失。 但是這樣的壞處也是很?的, 主要的壞處就是操作被“分片” 了, 代碼寫的不是 “一氣呵成” 這種。 而是每次來段數據就要判斷 數據夠不夠處理, 夠處理就處理, 不夠處理就再等等。 這樣代碼的可讀性很低, 其實也不符合?類的習慣。
但是協程可以很好解決這個問題。 比如 把?個IO操作 寫成?個協程。 當觸發IO操作的時候就自動讓出CPU給其他協程。 要知道協程的切換很輕的。 協程通過這種對異步IO的封裝 既保留了性能也保證了代碼的容易編寫和可讀性。在?IO密集型的程序下很好。 但是高CPU密集型的程序下沒啥好處。
4.1 協程的生成器實現
協程使用生成器來實現的,代碼如下(只切換了函數調用,所以效率比較高):
import timedef A():
while True:
print("----A---")
yield
time.sleep(0.5)
def B(c):
while True:
print("----B---")
c.next()
time.sleep(0.5)
if __name__=='__main__':
a = A()
B(a)
結果如下:
--B----A--
--B--
--A--
--B--
--A--
--B--
--A--
...省略...
4.2 協程的greenlet實現
與生成器實現類似。
注意:進程、線程的調用是操作系統決定的,執行順序不可預測,而協程是程序員決定的執行順序可預測,這由以下代碼可知(當執行到xx.switch()時會切換)。
使用下面命令安裝greenlet:
sudo pip install greenlet #python2的安裝方式sudo pip3 install greenlet #python3的安裝方式
協程的greenlet實現代碼如下:
#coding=utf-8from greenlet import greenlet
import time
def test1():
while True:
print "---A--"
gr2.switch() # 切換到gr2(即test2)中執行,test2執行切換時會從當前接著執行
time.sleep(0.5)
def test2():
while True:
print "---B--"
gr1.switch() # 切換到gr1(即test1)中執行,test1執行切換時會從當前接著執行
time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
#切換到gr1(即test1函數)中執行
gr1.switch()
結果如下:
--A----B--
--A--
--B--
--A--
--B--
--A--
...省略...
4.3 協程的gevent實現
gevent是對greenlet的再次封裝,不用程序員自己編程切換,當遇到需要切換的地方會自動切換。
4.3.1 gevent的使用
#coding=utf-8#請使?python 2 來執?此程序
import gevent
def f(n):
for i in range(n):
print gevent.getcurrent(), i
g1 = gevent.spawn(f, 5) # 綁定f函數,執行5次
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
# 清除協程
g1.join()
g2.join()
g3.join()
執行結果:
一瞬間執行完畢,g1、g2、g3依次順序執行,并非交替執行,不是我們想要的結果。
<Greenlet at 0x10e49f550: f(5)> 0<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4
4.3.2 gevent的切換執行
上面順序執行的原因是在f函數中沒有調用延時,所以不會切換。gevent當遇到耗時操作時才會切換,所以增加一個延時函數使它能夠切換,代碼如下:
import geventdef f(n):
for i in range(n):
print gevent.getcurrent(), i
#?來模擬?個耗時操作, 注意不是time模塊中的sleep
gevent.sleep(1)
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
結果如下:
<Greenlet at 0x7fa70ffa1c30: f(5)> 0<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4
4.3.3 gevent的服務器實現
注意:要使用gevent實現服務器,不能使用默認的socket,而是使用gevent自己的socket,gevent將常用的耗時操作都重寫了一遍,用于檢測是否為耗時操作。
import sysimport time
import gevent
from gevent import socket,monkey
# 此語句會將本代碼改寫,位于編譯器級的,具體不清楚!(python為動態語言在執行中可以修改)
# 必須使用!!!
monkey.patch_all()
def handle_request(conn):
while True:
#--------------#1處#-----------------
data = conn.recv(1024) # 這是gevent中的recv,為耗時操作,會切換到2處!
if not data:
conn.close()
break
print("recv:", data)
conn.send(data)
def server(port):
s = socket.socket()
s.bind(('', port))
s.listen(5)
while True:
#--------------#2處#-----------------
cli, addr = s.accept() # 這是gevent中的accept,為耗時操作,會進行切換!
# 注意:第一次到這里時只有一個協程,不需要切換,在此等待!
# 不為第一次時切換到1處!
gevent.spawn(handle_request, cli)
if __name__ == '__main__':
server(7788)
總結
以上是生活随笔為你收集整理的socket编程listen函数限制连接数的解决方案_网络编程——服务器篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: private修饰的变量如何调用_梳理c
- 下一篇: python爬虫论文摘要怎么写_Pyth