溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

從零實(shí)現(xiàn)一個(gè)http服務(wù)器

發(fā)布時(shí)間:2020-06-16 10:29:12 來源:網(wǎng)絡(luò) 閱讀:2190 作者:張小方32 欄目:建站服務(wù)器

我始終覺得,天生的出身很重要,但后天的努力更加重要,所以如今的很多“科班”往往不如后天努力的“非科班”。所以,我們需要重新給“專業(yè)”和“專家”下一個(gè)定義:所謂專業(yè),就是別人搞你不搞,這就是你的“專業(yè)”;你和別人同時(shí)搞,你比別人搞的好,就是“專家”。

說到http協(xié)議和http請求,很多人都知道,但是他們真的“知道”嗎?我面試過很多求職者,一說到http協(xié)議,他們能滔滔不絕,然后我問他http協(xié)議的具體格式是啥樣子的?很多人不清楚,不清楚就不清楚吧,他甚至能將http協(xié)議的頭扯到html文檔頭部<head>。當(dāng)我問http GET和POST請求的時(shí)候,GET請求是什么形式一般人都可以答出來,但是POST請求的數(shù)據(jù)放在哪里,服務(wù)器如何識別和解析這些POST數(shù)據(jù),很多人又說不清道不明了。當(dāng)說到http服務(wù)器時(shí),很多人離開了apache、Nginx這樣現(xiàn)成的http server之外,自己實(shí)現(xiàn)一個(gè)http服務(wù)器無從下手,如果實(shí)際應(yīng)用場景有需要使用到一些簡單http請求時(shí),使用apache、Nginx這樣重量級的http服務(wù)器程序?qū)嵲趧趲焺?dòng)眾,你可以嘗試自己實(shí)現(xiàn)一個(gè)簡單的。

上面提到的問題,如果您不能清晰地回答出來,可以閱讀一下這篇文章,這篇文章在不僅介紹http的格式,同時(shí)帶領(lǐng)大家從零實(shí)現(xiàn)一個(gè)簡單的http服務(wù)器程序。

一、項(xiàng)目背景

最近很多朋友希望我的flamingo服務(wù)器支持http協(xié)議,我自己也想做一個(gè)微信小程序,小程序通過http協(xié)議連接通過我的flamingo服務(wù)器進(jìn)行聊天。flamingo是一個(gè)開源的即時(shí)通訊軟件,目前除了服務(wù)器端,還有pc端、android端,后面會支持更多的終端。關(guān)于flamingo的介紹您可以參考這里:https://blog.csdn.net/analogous_love/article/details/69481542,這是我不斷維護(hù)一個(gè)項(xiàng)目,其最新代碼下載地址是:https://github.com/baloonwj/flamingo,更新日志:https://github.com/baloonwj/flamingo/issues/1。下面是flamingo的部分截圖:
從零實(shí)現(xiàn)一個(gè)http服務(wù)器
二、http協(xié)議介紹

  1. http協(xié)議是應(yīng)用層協(xié)議,一般建立在tcp協(xié)議的基礎(chǔ)之上(當(dāng)然你的實(shí)現(xiàn)非要基于udp也是可以的),也就是說http協(xié)議的數(shù)據(jù)收發(fā)是通過tcp協(xié)議的。

  2. http協(xié)議也分為head和body兩部分,但是我們一般說的html中的<head>和<body>標(biāo)記不是http協(xié)議的頭和身體,它們都是http協(xié)議的body部分。
    從零實(shí)現(xiàn)一個(gè)http服務(wù)器

那么http協(xié)議的頭到底長啥樣子呢?我們來介紹一下http協(xié)議吧。

http協(xié)議的格式如下:

GET或POST 請求的url路徑(一般是去掉域名的路徑) HTTP協(xié)議版本號
字段1名: 字段1值\r\n
字段2名: 字段2值\r\n
      ...
字段n名 : 字段n值\r\n
\r\n
http協(xié)議包體內(nèi)容

也就是說http協(xié)議由兩部分組成:包頭和包體,包頭與包體之間使用一個(gè)\r\n分割,由于http協(xié)議包頭的每一行都是以\r\n結(jié)束,所以http協(xié)議包頭一般以\r\n\r\n結(jié)束。

舉個(gè)例子,比如我們在瀏覽器中請求http://www.hootina.org/index_2013.php這個(gè)網(wǎng)址,這是一個(gè)典型的GET方法,瀏覽器組裝的http數(shù)據(jù)包格式如下:

GET /index_2013.php HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
\r\n

上面這個(gè)請求只有包頭沒有包體,http協(xié)議的包體不是必須的,也就是說GET請求一般沒有包體。

如果GET請求帶參數(shù),那么一般是附加在請求的url后面,參數(shù)與參數(shù)之間使用&分割,例如請求http://www.hootina.org/index_2013.php?param1=value1¶m2=value2¶m3=value3,我們看下這個(gè)請求組裝的的http協(xié)議包格式:

GET /index_2013.php?param1=value1&param2=value2&param3=value3 HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
\r\n

對比一下,你現(xiàn)在知道http協(xié)議的GET參數(shù)放在協(xié)議包的什么位置了吧。

那么POST的數(shù)據(jù)放在什么位置呢?我們再12306網(wǎng)站(https://kyfw.12306.cn/otn/login/init)中登陸輸入用戶名和密碼:

從零實(shí)現(xiàn)一個(gè)http服務(wù)器

然后發(fā)現(xiàn)瀏覽器以POST方式組裝了http協(xié)議包發(fā)送了我們的用戶名、密碼和其他一些信息,組裝的包格式如下:

POST /passport/web/login HTTP/1.1\r\n
Host: kyfw.12306.cn\r\n
Connection: keep-alive\r\n
Content-Length: 55\r\n
Accept: application/json, text/javascript, */*; q=0.01\r\n
Origin: https://kyfw.12306.cn\r\n
X-Requested-With: XMLHttpRequest\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n
Referer: https://kyfw.12306.cn/otn/login/init\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
Cookie: _passport_session=0b2cc5b86eb74bcc976bfa9dfef3e8a20712; _passport_ct=18d19b0930954d76b8057c732ce4cdcat8137; route=6f50b51faa11b987e576cdb301e545c4; RAIL_EXPIRATION=1526718782244; RAIL_DEVICEID=QuRAhOyIWv9lwWEhkq03x5Yl_livKZxx7gW6_-52oTZQda1c4zmVWxdw5Zk79xSDFHe9LJ57F8luYOFp_yahxDXQAOmEV8U1VgXavacuM2UPCFy3knfn42yTsJM3EYOy-hwpsP-jTb2OXevJj5acf40XsvsPDcM7; BIGipServerpool_passport=300745226.50215.0000; BIGipServerotn=1257243146.38945.0000; BIGipServerpassport=1005060362.50215.0000\r\n
\r\n
username=balloonwj%40qq.com&password=iloveyou&appid=otn

其中username=balloonwj%40qq.com&password=iloveyou&appid=otn就是我們的POST數(shù)據(jù),但是大家需要注意的以下幾種,不要搞錯(cuò):

  1. 我的用戶名是balloonwj@qq.com,到POST里面變成balloonwj%40qq.com,其中%40是@符號的16進(jìn)制轉(zhuǎn)碼形式。這個(gè)碼表可以參考這里:http://www.w3school.com.cn/tags/html_ref_urlencode.html

2.這里有三個(gè)變量,分別是username、password和appid,他們之間使用&符號分割,但是請注意的是,這不意味著傳遞多個(gè)POST變量時(shí)必須使用&符號分割,只不過這里是瀏覽器html表單(輸入用戶名和密碼的文本框是html表單的一種)分割多個(gè)變量采用的默認(rèn)方式而已。你可以根據(jù)你的需求,來自由定制,只要讓服務(wù)器知道你的解析方式即可。比如可以這么分割:

方法一:
username=balloonwj%40qq.com|password=iloveyou|appid=otn

方法二:
username:balloonwj%40qq.com\r\n
password:iloveyou\r\n
appid:otn\r\n

方法三
username,password,appid=balloonwj%40qq.com,iloveyou,otn
不管怎么分割,只要你能自己按一定的規(guī)則解析出來就可以了。

不知道你注意到?jīng)]有,上面的POST數(shù)據(jù)放在http包體中,服務(wù)器如何解析呢?可能你沒明白我的意思,看下圖:

如上圖所示,由于http協(xié)議是基于tcp協(xié)議的,tcp協(xié)議是流式協(xié)議,包頭部分可以通過多出的\r\n來分界,包體部分如何分界呢?這是協(xié)議本身要解決的問題。目前一般有兩種方式,第一種方式就是在包頭中有個(gè)content-Length字段,這個(gè)字段的值的大小標(biāo)識了POST數(shù)據(jù)的長度,上圖中55就是數(shù)據(jù)username=balloonwj%40qq.com&password=iloveyou&appid=otn的長度,服務(wù)器收到一個(gè)數(shù)據(jù)包后,先從包頭解析出這個(gè)字段的值,再根據(jù)這個(gè)值去讀取相應(yīng)長度的作為http協(xié)議的包體數(shù)據(jù)。還有一個(gè)格式叫做http chunked技術(shù)(分塊),大致意思是將大包分成小包,具體的詳情有興趣的讀者可以自行搜索學(xué)習(xí)。

三、http客戶端實(shí)現(xiàn)

如果您能掌握以上說的http協(xié)議,你就可以自己通過代碼組裝http協(xié)議發(fā)送http請求了(也是各種開源http庫的做法)。我們先簡單地介紹一下如何模擬發(fā)送http。舉個(gè)例子,我們要請求http://www.hootina.org/index_2013.php,那么我們可以先通過域名得到ip地址,即通過socket API gethostbyname()得到www.hootina.org的ip地址,由于http服務(wù)器默認(rèn)的端口號是80,有了域名和ip地址之后,我們使用socket API connect()去連接服務(wù)器,然后根據(jù)上面介紹的格式組裝成http協(xié)議包,利用socket API send()函數(shù)發(fā)出去,如果服務(wù)器有應(yīng)答,我們可以使用socket API recv()去接受數(shù)據(jù),接下來就是解析數(shù)據(jù)(先解析包頭和包體)。

四、http服務(wù)器實(shí)現(xiàn)

我們這里簡化一些問題,假設(shè)客戶端發(fā)送的請求都是GET請求,當(dāng)客戶端發(fā)來http請求之后,我們拿到http包后就做相應(yīng)的處理。我們以為我們的flamingo服務(wù)器實(shí)現(xiàn)一個(gè)支持http格式的注冊請求為例。假設(shè)用戶在瀏覽器里面輸入以下網(wǎng)址,就可以實(shí)現(xiàn)一個(gè)注冊功能:

http://120.55.94.78:12345/register.do?p={"username": "13917043329", "nickname": "balloon", "password": "123"}

這里我們的http協(xié)議使用的是12345端口號而不是默認(rèn)的80端口。如何偵聽12345端口,這個(gè)是非?;A(chǔ)的知識了,這里就不介紹了。當(dāng)我們收到數(shù)據(jù)以后:

void HttpSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime)
{
    //LOG_INFO << "Recv a http request from " << conn->peerAddress().toIpPort();

    string inbuf;
    //先把所有數(shù)據(jù)都取出來
    inbuf.append(pBuffer->peek(), pBuffer->readableBytes());
    //因?yàn)橐粋€(gè)http包頭的數(shù)據(jù)至少\r\n\r\n,所以大于4個(gè)字符
    //小于等于4個(gè)字符,說明數(shù)據(jù)未收完,退出,等待網(wǎng)絡(luò)底層接著收取
    if (inbuf.length() <= 4)
        return;

    //我們收到的GET請求數(shù)據(jù)包一般格式如下:
    /*
    GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1\r\n
    Host: 120.55.94.78:12345\r\n
    Connection: keep-alive\r\n
    Upgrade-Insecure-Requests: 1\r\n
    User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
    Accept-Encoding: gzip, deflate\r\n
    Accept-Language: zh-CN, zh; q=0.9, en; q=0.8\r\n
    \r\n
     */
    //檢查是否以\r\n\r\n結(jié)束,如果不是說明包頭不完整,退出
    string end = inbuf.substr(inbuf.length() - 4);
    if (end != "\r\n\r\n")
        return;

    //以\r\n分割每一行
    std::vector<string> lines;
    StringUtil::Split(inbuf, lines, "\r\n");
    if (lines.size() < 1 || lines[0].empty())
    {
        conn->forceClose();
        return;
    }

    std::vector<string> chunk;
    StringUtil::Split(lines[0], chunk, " ");
    //chunk中至少有三個(gè)字符串:GET+url+HTTP版本號
    if (chunk.size() < 3)
    {
        conn->forceClose();
        return;
    }

    LOG_INFO << "url: " << chunk[1] << " from " << conn->peerAddress().toIpPort();
    //inbuf = /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22}
    std::vector<string> part;
    //通過?分割成前后兩端,前面是url,后面是參數(shù)
    StringUtil::Split(chunk[1], part, "?");
    //chunk中至少有三個(gè)字符串:GET+url+HTTP版本號
    if (part.size() < 2)
    {
        conn->forceClose();
        return;
    }

    string url = part[0];
    string param = part[1].substr(2);

    if (!Process(conn, url, param))
    {
        LOG_ERROR << "handle http request error, from:" << conn->peerAddress().toIpPort() << ", request: " << pBuffer->retrieveAllAsString();
    }

    //短連接,處理完關(guān)閉連接
    conn->forceClose();
}

代碼注釋都寫的很清楚,我們先利用\r\n分割得到每一行,其中第一行的數(shù)據(jù)是:

GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1

其中%22是雙引號的url轉(zhuǎn)碼形式,%20是空格的url轉(zhuǎn)碼形式,然后我們根據(jù)空格分成三段,其中第二段就是我們的網(wǎng)址和參數(shù):

/register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22}

然后我們根據(jù)網(wǎng)址與參數(shù)之間的問號將這個(gè)分成兩段:第一段是網(wǎng)址,第二段是參數(shù):

bool HttpSession::Process(const std::shared_ptr<TcpConnection>& conn, const std::string& url, const std::string& param)
{
    if (url.empty())
        return false;

    if (url == "/register.do")
    {
        OnRegisterResponse(param, conn);
    }
    else if (url == "/login.do")
    {
        OnLoginResponse(param, conn);
    }
    else if (url == "/getfriendlist.do")
    {

    }
    else if (url == "/getgroupmembers.do")
    {

    }
    else
        return false;

    return true;
}

然后我們根據(jù)url匹配網(wǎng)址,如果是注冊請求,會走注冊處理邏輯:

void HttpSession::OnRegisterResponse(const std::string& data, const std::shared_ptr<TcpConnection>& conn)
{
    string retData;
    string decodeData;
    URLEncodeUtil::Decode(data, decodeData);
    BussinessLogic::RegisterUser(decodeData, conn, false, retData);
    if (!retData.empty())
    {
        std::string response;
        URLEncodeUtil::Encode(retData, response);
        MakeupResponse(retData, response);
        conn->send(response);

        LOG_INFO << "Response to client: cmd=msg_type_register" << ", data=" << retData << conn->peerAddress().toIpPort();;
    }
}

注冊結(jié)果放在retData中,為了發(fā)給客戶端,我們將結(jié)果中的特殊字符如雙引號轉(zhuǎn)碼,如返回結(jié)果是:

{"code":0, "msg":"ok"}
會被轉(zhuǎn)碼成:

{%22code%22:0,%20%22msg%22:%22ok%22}
然后,將數(shù)據(jù)組裝成http協(xié)議發(fā)給客戶端,給客戶端的應(yīng)答協(xié)議與http請求協(xié)議有一點(diǎn)點(diǎn)差別,就是將請求的url路徑換成所謂的http響應(yīng)碼,如200表示應(yīng)答正常返回、404頁面不存在。應(yīng)答協(xié)議格式如下:

GET或POST 響應(yīng)碼 HTTP協(xié)議版本號
字段1名: 字段1值\r\n
字段2名: 字段2值\r\n
      ...
字段n名 : 字段n值\r\n
\r\n
http協(xié)議包體內(nèi)容

舉個(gè)例子如:

HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Content-Length:42\r\n
\r\n
{%22code%22:%200,%20%22msg%22:%20%22ok%22}

注意,包頭中的Content-Length長度必須正好是包體{%22code%22:%200,%20%22msg%22:%20%22ok%22}的長度,這里是42。這也符合我們?yōu)g覽器的返回結(jié)果:

當(dāng)然,需要注意的是,我們一般說http連接一般是短連接,這里我們也實(shí)現(xiàn)了這個(gè)功能(看上面的代碼:conn->forceClose();),不管一個(gè)http請求是否成功,服務(wù)器處理后立馬就關(guān)閉連接。

當(dāng)然,這里還有一些沒處理好的地方,如果你仔細(xì)觀察上面的代碼就會發(fā)現(xiàn)這個(gè)問題,就是不滿足一個(gè)http包頭時(shí)的處理,如果某個(gè)客戶端(不是使用瀏覽器)通過程序模擬了一個(gè)連接請求,但是遲遲不發(fā)含有\(zhòng)r\n\r\n的數(shù)據(jù),這路連接將會一直占用。我們可以判斷收到的數(shù)據(jù)長度,防止別有用心的客戶端給我們的服務(wù)器亂發(fā)數(shù)據(jù)。我們假定,我們能處理的最大url長度是2048,如果用戶發(fā)送的數(shù)據(jù)累積不含\r\n\r\n,且超過2048個(gè),我們認(rèn)為連接非法,將連接斷開。代碼修改成如下形式:

void HttpSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime)
{
    //LOG_INFO << "Recv a http request from " << conn->peerAddress().toIpPort();

    string inbuf;
    //先把所有數(shù)據(jù)都取出來
    inbuf.append(pBuffer->peek(), pBuffer->readableBytes());
    //因?yàn)橐粋€(gè)http包頭的數(shù)據(jù)至少\r\n\r\n,所以大于4個(gè)字符
    //小于等于4個(gè)字符,說明數(shù)據(jù)未收完,退出,等待網(wǎng)絡(luò)底層接著收取
    if (inbuf.length() <= 4)
        return;

    //我們收到的GET請求數(shù)據(jù)包一般格式如下:
    /*
    GET /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22} HTTP/1.1\r\n
    Host: 120.55.94.78:12345\r\n
    Connection: keep-alive\r\n
    Upgrade-Insecure-Requests: 1\r\n
    User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n
    Accept-Encoding: gzip, deflate\r\n
    Accept-Language: zh-CN, zh; q=0.9, en; q=0.8\r\n
    \r\n
     */
    //檢查是否以\r\n\r\n結(jié)束,如果不是說明包頭不完整,退出
    string end = inbuf.substr(inbuf.length() - 4);
    if (end != "\r\n\r\n")
        return;
    //超過2048個(gè)字符,且不含\r\n\r\n,我們認(rèn)為是非法請求
    else if (inbuf.length() >= MAX_URL_LENGTH)
    {
        conn->forceClose();
        return;
    }

    //以\r\n分割每一行
    std::vector<string> lines;
    StringUtil::Split(inbuf, lines, "\r\n");
    if (lines.size() < 1 || lines[0].empty())
    {
        conn->forceClose();
        return;
    }

    std::vector<string> chunk;
    StringUtil::Split(lines[0], chunk, " ");
    //chunk中至少有三個(gè)字符串:GET+url+HTTP版本號
    if (chunk.size() < 3)
    {
        conn->forceClose();
        return;
    }

    LOG_INFO << "url: " << chunk[1] << " from " << conn->peerAddress().toIpPort();
    //inbuf = /register.do?p={%22username%22:%20%2213917043329%22,%20%22nickname%22:%20%22balloon%22,%20%22password%22:%20%22123%22}
    std::vector<string> part;
    //通過?分割成前后兩端,前面是url,后面是參數(shù)
    StringUtil::Split(chunk[1], part, "?");
    //chunk中至少有三個(gè)字符串:GET+url+HTTP版本號
    if (part.size() < 2)
    {
        conn->forceClose();
        return;
    }

    string url = part[0];
    string param = part[1].substr(2);

    if (!Process(conn, url, param))
    {
        LOG_ERROR << "handle http request error, from:" << conn->peerAddress().toIpPort() << ", request: " << pBuffer->retrieveAllAsString();
    }

    //短連接,處理完關(guān)閉連接
    conn->forceClose();
}

但這只能解決發(fā)送非法數(shù)據(jù)的情況,如果一個(gè)客戶端連上來不給我們發(fā)任何數(shù)據(jù),這段邏輯就無能為力了。如果不斷有客戶端這么做,會浪費(fèi)我們大量的連接資源,所以我們還需要一個(gè)定時(shí)器去定時(shí)檢測哪些http連接超過一定時(shí)間內(nèi)沒給我們發(fā)數(shù)據(jù),找到后將連接斷開。這又涉及到服務(wù)器定時(shí)器如何設(shè)計(jì)了,關(guān)于這部分請參考我寫的其他文章。

限于作者經(jīng)驗(yàn)水平有限,文中難免有錯(cuò)亂之處,歡迎拍磚。另外,關(guān)于上面的代碼,可以去github上下載,地址是:

https://github.com/baloonwj/flamingo

全文完。

歡迎關(guān)注公眾號『easyserverdev』。如果有任何技術(shù)或者職業(yè)方面的問題需要我提供幫助,可通過這個(gè)公眾號與我取得聯(lián)系,此公眾號不僅分享高性能服務(wù)器開發(fā)經(jīng)驗(yàn)和故事,同時(shí)也免費(fèi)為廣大技術(shù)朋友提供技術(shù)答疑和職業(yè)解惑,您有任何問題都可以在微信公眾號直接留言,我會盡快回復(fù)您。
從零實(shí)現(xiàn)一個(gè)http服務(wù)器

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI