日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

从简单到高并发服务器(一)

發(fā)布時(shí)間:2025/7/14 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从简单到高并发服务器(一) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一個(gè)單線程的回聲服務(wù)器 (Echo Server)

我們從一個(gè)簡單的服務(wù)器開始說起。
它可以接受一個(gè)客戶的連接,接收消息,然后把這個(gè)消息發(fā)送回去,關(guān)閉連接——完工。我們用 Linux 和 iOS / OSX 上都通用的 BSD Socket 來編寫這個(gè)服務(wù)器的代碼。主體部分大概是這樣的:(C++ 語法)

#include <stdio.h> #include <stdlib.h>#include <netdb.h> #include <netinet/in.h>#include <unistd.h> #include <string.h>void fuck_you(void) {exit(EXIT_FAILURE); };int main(int argc, char *argv[]) {int server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);struct sockaddr_in server_addr;struct sockaddr_in client_addr;int portno = 5432;bzero((char *) &server_addr, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(portno);// 注1: 將 socket 綁定到具體的 IP 地址int bind_succ = bind( server_sock, (struct sockaddr *) &server_addr, sizeof(server_addr));if (bind_succ < 0) fuck_you();// 注2: 開始監(jiān)聽int listen_succ = listen(server_sock, 1024);if (listen_succ < 0) fuck_you();// 注3: 這是一個(gè)永遠(yuǎn)不會(huì)停止的循環(huán)while(true){int client_addr_size;printf("before accept.\n");// 注4: 調(diào)用 accept 等待傳入連接的時(shí)候會(huì)阻塞 (the thread is blocked here!)int client_sock = accept( server_sock // 從這個(gè)服務(wù)器 socket 上面接受連接, (struct sockaddr *) &client_addr // 這里獲得發(fā)起連接的客戶端的地址, (socklen_t*)& client_addr_size);printf("handle client sock: %d", client_sock);// 準(zhǔn)備用于接收消息的緩沖區(qū)const int buffer_size = 1024;char * recv_buffer = (char*)malloc(buffer_size);printf("before recv, buffer ready, address: %p\n", recv_buffer);// 注5: 調(diào)用 recv 來接收客戶端發(fā)送的信息,這個(gè)過程會(huì)阻塞int msg_size = recv( client_sock // 注意,是用 client_sock 來接收, recv_buffer // 將接收到的內(nèi)容放到緩沖區(qū), buffer_size // 告訴系統(tǒng)我們設(shè)置的緩沖區(qū)有多大, 0);fwrite(recv_buffer, sizeof(char), msg_size, stdout);// 注6: 把接收到的信息原樣發(fā)送出去int byte_sent = send( client_sock, recv_buffer, msg_size, 0);free(recv_buffer);// 注7: 主動(dòng)斷開客戶端的連接close(client_sock);} }

這段代碼當(dāng)然是很粗糙(誤:粗口),可能會(huì)有內(nèi)存泄漏,如果客戶發(fā)送的消息過長會(huì)接收不完全……各種各樣的問題,但是它基本上呈現(xiàn)出了一個(gè)服務(wù)器程序到底是怎樣運(yùn)作的。

以下是代碼中提到的,要實(shí)現(xiàn)一個(gè)TCP服務(wù)器幾個(gè)重要的工作:

  • 綁定監(jiān)聽地址,并開始監(jiān)聽(注1和注2)

  • 等待客戶端連接(注4)

  • 接收客戶端發(fā)送的數(shù)據(jù)(注5)

  • 發(fā)送回復(fù)(注6)

  • 實(shí)際上以上這四點(diǎn)也是任何服務(wù)器都要完成的事情。
    如果是使用 Udp 的話,則不需要等待客戶端連接這個(gè)步驟,這是因?yàn)?Udp 是面向數(shù)據(jù)包而不是面向連接的傳輸協(xié)議;而使用 Tcp 則需要等待客戶端連接,實(shí)際上還會(huì)涉及到“三路握手” (3-way handshake) 這個(gè)建立 Tcp 連接的過程。
    但是這個(gè)握手過程,由于是屬于 TCP 協(xié)議的標(biāo)準(zhǔn)部分,因此實(shí)際上是由操作系統(tǒng)來幫助我們完成的(所有支持 TCP/IP 協(xié)議棧的操作系統(tǒng)都會(huì)替程序員完成這個(gè)過程)。我們只需要通過調(diào)用 accept 這個(gè)API,就相當(dāng)于告訴系統(tǒng)“現(xiàn)在開始幫我處理握手這個(gè)事情,有人找你握手了再來告訴我吧”。

    線程與阻塞

    握手過程調(diào)用 accept 會(huì)阻塞整個(gè)程序的執(zhí)行,阻塞是什么意思呢?
    如果我們寫代碼的時(shí)候,寫一個(gè)死循環(huán),就如代碼中 注3 那樣:

    while(true) {printf("I just can't stop speaking!\n"); }

    即使不運(yùn)行這個(gè)程序,你也應(yīng)該可以預(yù)料到,在屏幕上會(huì)不斷打出一行行的內(nèi)容。這說明,程序沒有被阻塞的情況下,就會(huì)一直執(zhí)行下去。嚴(yán)格來說,printf 也會(huì)阻塞,只不過阻塞的時(shí)間非常短,并且可以自動(dòng)解除阻塞狀態(tài),具體的解釋以后再說。

    而調(diào)用 accept 就不可以自動(dòng)解除阻塞狀態(tài)了——如果你成功運(yùn)行剛才的代碼,你會(huì)看到,屏幕輸出了 before accept. 之后,并沒有馬上接著輸出 handle client sock: ——程序一直停留在 accept 被調(diào)用的地方,也可以認(rèn)為是 accept 一直沒有返回結(jié)果。

    阻塞的本質(zhì)是,操作系統(tǒng)把執(zhí)行你的代碼的線程暫停了,而線程則是操作系統(tǒng)安排CPU調(diào)度的基本單位,這通常意味著操作系統(tǒng)把 CPU 拿去干其他事情了,而你的程序不能使用 CPU進(jìn)行計(jì)算,只能暫停。直到有一個(gè)客戶成功連接到你的服務(wù)器為止。

    為了模擬這個(gè)事情,我們可以使用 python + gevent 來模擬很多(300)個(gè)客戶端并發(fā)地不停發(fā)起TCP連接:

    from __future__ import print_function from gevent.socket import socket as gsocket import gevent import socketdef do_connect(addr, index):if 0: client_sock = socket.socket()while True:client_sock = gsocket(socket.AF_INET,socket.SOCK_STREAM,socket.IPPROTO_TCP)print(addr)client_sock.connect(addr)print('client {0} connected.'.format(index))gevent.sleep(10)client_sock.send('Hello World')data = client_sock.recv(1024)print('recv data: {0}'.format(data))if __name__ == '__main__':server_addr = ('127.0.0.1', 5432)greenlets = list()for i in xrange(300):g = gevent.spawn(do_connect, server_addr, i)greenlets.append(g)gevent.joinall(greenlets)

    然后,如無意外,你就可以看到程序繼續(xù)得到執(zhí)行,非常有規(guī)律地重復(fù),并且總是按順序地一個(gè)連接一個(gè)連接地處理。如果注意到客戶端的輸出話你可能會(huì)看到,在后面的發(fā)起的連接都超時(shí)了,會(huì)看到很多 Traceback。

    這肯定不是我們?nèi)粘TL問網(wǎng)站所能得到的體驗(yàn):很快就可以連接上并且看到網(wǎng)頁的內(nèi)容(當(dāng)然,在天朝,例外有很多)。所以這不是理想的高并發(fā)服務(wù)器。

    為什么比較早發(fā)起連接的客戶端不會(huì)超時(shí),而后面發(fā)起的會(huì)超時(shí)呢?原因就是服務(wù)器端在阻塞等待 IO 的時(shí)候,單線程無法響應(yīng)其他請求。

    為了驗(yàn)證這個(gè)結(jié)論,你可以把客戶端代碼中,發(fā)送數(shù)據(jù)前 gevent.sleep 的時(shí)間加長,例如改為20 秒,你會(huì)發(fā)現(xiàn)更多的連接會(huì)超時(shí)—— 因?yàn)榉?wù)器花費(fèi)在等待客戶端發(fā)送數(shù)據(jù)的時(shí)間更多了,那么在相同超時(shí)時(shí)間前服務(wù)窗口內(nèi)能夠 accept 的連接數(shù)量就更少了。

    (未完待續(xù))

    總結(jié)

    以上是生活随笔為你收集整理的从简单到高并发服务器(一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。