怎么才能百度做網(wǎng)站百度一下官網(wǎng)手機版
目錄
前提知識
1. 理解源ip,目的ip和Macip
2. 端口號
3. 初識TCP,UDP協(xié)議
4.?網(wǎng)絡(luò)字節(jié)序
5. socket 編程
sockaddr類型?
一,基于udp協(xié)議編程?
1. socket——創(chuàng)建套接字
2. bind——將套接字強綁定?
3. recvfrom——接受數(shù)據(jù)
4. sendto——發(fā)出信息
?遇到的問題
(1. 云服務(wù)器中以及無法分配IP問題
(2. IP:127.0.0.1更深層次的認(rèn)識
(3. 關(guān)于服務(wù)端bind的優(yōu)化
源碼
二,基于tcp協(xié)議編程
1. listen——服務(wù)端監(jiān)聽
2. accept——服務(wù)端接收
3. connect——客戶端請求
4. send & recev
三,基于TCP協(xié)議實現(xiàn)的線程池的服務(wù)端對客戶端進行相互通信小項目
結(jié)構(gòu)圖一覽
源碼?
1. 單進程處理
2.?子進程處理
2.1.?孫子進程處理
3. 多線程處理
4. 線程池處理
下期:TCP協(xié)議原理
結(jié)語
嗨!收到一張超美的風(fēng)景圖,愿你每天都能順心!?
前提知識
1. 理解源ip,目的ip和Macip
數(shù)據(jù)在以太網(wǎng)上傳輸,經(jīng)過多個路由器,Mac地址多次封裝解包是變化的(可以理解為數(shù)據(jù)的下一個主機地址),而源ip,尤其是目的ip一般是不會改變。
2. 端口號
- 端口號是一個2字節(jié)16位的整數(shù)(uint16_t);
- 端口號用來標(biāo)識一個進程, 告訴操作系統(tǒng), 當(dāng)前的這個數(shù)據(jù)要交給哪一個進程來處理;
- 一個端口號只能被一個進程占用(標(biāo)識一臺主機進程的唯一性)
理解:假設(shè)客戶發(fā)送操作在終端應(yīng)用為A,在另一臺機器的服務(wù)器應(yīng)用為B,A向B發(fā)送操作請求,本質(zhì)上是不同機器之間進程間通信,請求數(shù)據(jù)經(jīng)過封裝,傳遞,解包后,B所在的操作系統(tǒng)將數(shù)據(jù)根據(jù)端口號,交給那個進程處理。
同理,我們就能理解源端口號,就是發(fā)出數(shù)據(jù)的進程;目的端口號,處理數(shù)據(jù)的進程 。
因此,IP地址 + 端口號能夠標(biāo)識網(wǎng)絡(luò)上的唯一臺主機的唯一一個進程;
注意:
1.一個進程可以有多個端口號綁定,但一個端口不能被多個進程綁定。
2. pid是系統(tǒng)管理進程的唯一標(biāo)識符,與端口號沒有聯(lián)系。
同時,{IP地址 + 端口號}的模式被叫做套接字,網(wǎng)絡(luò)通信用套接字的方法實現(xiàn),網(wǎng)絡(luò)編程,也可以被叫做套接字編程。
3. 初識TCP,UDP協(xié)議
首先我們來找找他們傳輸層上的這兩協(xié)議
?
這里我們只了解兩協(xié)議的特點,具體我們后面再結(jié)合場景理解?
各自特點:
TCP(Transmission Control Protocol 傳輸控制協(xié)議)
- 傳輸層協(xié)議
- 有連接(是否需要手動連接)——神似打電話
- 可靠傳輸(對數(shù)據(jù)包檢測,丟包重傳等等)
- 面向字節(jié)流(后面再提)
UDP(User Datagram Protocol 用戶數(shù)據(jù)報協(xié)議)
- 傳輸層協(xié)議
- 無連接(不用連接就可以發(fā)送)——神似發(fā)郵件
- 不可靠傳輸(不關(guān)心是否丟包)
- 面向數(shù)據(jù)報(后面再說)
4.?網(wǎng)絡(luò)字節(jié)序
說到字節(jié)序,我們是否想到C語言中學(xué)過的大小端字節(jié)序,那個是數(shù)據(jù)在內(nèi)存中的存儲方式。(大小端可參見:整型,浮點型深刻理解【C語言】【整型 || 原,反,補碼 || 浮點型 || 大小端字節(jié)序】_小端浮點數(shù)-CSDN博客)
現(xiàn)在我們討論的則是數(shù)據(jù)在向網(wǎng)絡(luò)發(fā)送時,是從低字節(jié)向高地址發(fā)送(小端),還是從高字節(jié)向高地址發(fā)送(大端)。
- 發(fā)送主機通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出;
- 接收主機把從網(wǎng)絡(luò)上接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序保存;
- 因此,網(wǎng)絡(luò)數(shù)據(jù)流的地址應(yīng)這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址.
- TCP/IP協(xié)議規(guī)定,網(wǎng)絡(luò)數(shù)據(jù)流應(yīng)采用大端字節(jié)序,即低地址高字節(jié).
- 不管這臺主機是大端機還是小端機, 都會按照這個TCP/IP規(guī)定的網(wǎng)絡(luò)字節(jié)序來發(fā)送/接收數(shù)據(jù);
- 如果當(dāng)前發(fā)送主機是小端, 就需要先將數(shù)據(jù)轉(zhuǎn)成大端; 否則就忽略, 直接發(fā)送即可

意思是:以htonl()為例,主機字節(jié)轉(zhuǎn)網(wǎng)絡(luò)字節(jié)序,返回數(shù)據(jù)。
5. socket 編程
首先我們先簡單了解常用的三中套接字:
- 域用socket? ? ? (基于網(wǎng)絡(luò)socket下的本地模式,類似于命名管道,可參見:進程通信知識基礎(chǔ)【Linux】——下篇【命名管道,共享內(nèi)存,信號量初識】-CSDN博客
- 原始socket? ? ?(一般用于一些工具制作——跳過傳輸層協(xié)議直接用于網(wǎng)絡(luò)層,甚至數(shù)據(jù)鏈路層)
- 網(wǎng)絡(luò)socket? ?
// 創(chuàng)建 socket 文件描述符 (TCP/UDP, 客戶端 + 服務(wù)器)
int socket(int domain, int type, int protocol);
// 綁定端口號 (TCP/UDP, 服務(wù)器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 開始監(jiān)聽socket (TCP, 服務(wù)器)
int listen(int socket, int backlog);
// 接收請求 (TCP, 服務(wù)器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立連接 (TCP, 客戶端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockaddr類型?

一,基于udp協(xié)議編程?
1. socket——創(chuàng)建套接字

?返回值:文件描述符作為返回值
?domain: 設(shè)置套接字類型(網(wǎng)絡(luò)通信,還是本地通信)

protocol: 一般根據(jù)前兩參數(shù)就決定好了,設(shè)置為0即可。?
2. bind——將套接字強綁定?
// 綁定端口號 (TCP/UDP, 服務(wù)器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
需要區(qū)別C++11中std::bind,后者是C++11的包裝器,用于函數(shù)參數(shù)管理。這里的是三種socket通信綁定套接字的通用接口,下面是實例:
sockaddr_in結(jié)構(gòu)體頭文件<netinet/in.h>
// 2.讓操作系統(tǒng)將該進程與我們的套接字進行強綁定,以便內(nèi)核中我們信息的獲取struct sockaddr_in local;// 全0填充,可以用memset,bzerobzero(&local, sizeof bzero);local.sin_family = AF_INET; // 設(shè)置通信類型// 服務(wù)器的ID和端口未來是要將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)中,數(shù)據(jù)就需要修改為大端。local.sin_port = htons(_port); // 對IP地址補充:// 常見的是"192.234.222.111"——點分十進制字符串風(fēng)格的IP地址,目的:用戶方便觀察// 分成四個領(lǐng)域,每個領(lǐng)域都是[0~255],也就是2^8,1字節(jié),所以IP,4字節(jié)可以完全表示// 如果網(wǎng)絡(luò)以上面字符串形式傳輸就是15字節(jié),所以需要15字節(jié) <-> 4字節(jié)(網(wǎng)絡(luò))local.sin_addr.s_addr = inet_addr(_id.c_str()); //4字節(jié)的ID也需要修改if (bind(socket_, (sockaddr*)&local, sizeof local) < 0){Logmessage(FATIL, "%d:%s\n", errno, strerror(errno));}
如果綁定成功,接下來主機就從可以從該套接字中接收數(shù)據(jù)。
常見的網(wǎng)絡(luò)轉(zhuǎn)換數(shù)據(jù)接口,具體用法問AI
man inet_addr
3. recvfrom——接受數(shù)據(jù)
參數(shù)解析:
- sockfd: 創(chuàng)建完的套接字返回文件標(biāo)識符——還是遵從一切為文件
- buf, len :信息緩沖區(qū)
- flags : 默認(rèn)為0,為阻塞方式接受信息
- src_addr, addrlen :一個輸出型參數(shù),收集發(fā)送者套接字(IP + 端口)。
- return :? 返回發(fā)送者發(fā)送字節(jié)數(shù)。
- sockaddr*, socklen_t* :兩類型是輸出式參數(shù),用于記錄發(fā)送方套接字
4. sendto——發(fā)出信息
需要注意的是socklen_t類型,需要
使用例子請看下面:
功能:一個客戶端,一個服務(wù)器,在同一臺機器上通過IP:127.0.0.1來實現(xiàn)互相數(shù)據(jù)發(fā)送。
//服務(wù)器啟動int start(){char buf[1024];while (1){//1.接受信息struct sockaddr_in peer;bzero(&peer, sizeof peer);socklen_t len = sizeof peer;ssize_t s_len = recvfrom(socket_, buf, (sizeof buf) - 1, 0, (struct sockaddr*)&peer, &len);if (s_len > 0){buf[s_len] = 0; // 協(xié)議分析,這里我們先不說//解析發(fā)送目標(biāo)uint16_t send_port = ntohs(peer.sin_port);std::string send_id = inet_ntoa(peer.sin_addr);printf("發(fā)送方 id:[%s] port:[%d]:%s\n",send_id.c_str(), send_port, buf);}//2.分析信息 //3.發(fā)出信息,我們選擇發(fā)送回sendto(socket_, buf, sizeof buf, 0, (struct sockaddr*)&peer, len);} }
(詳細代碼鏈接,我將放到文章末尾)?
?遇到的問題
(1. 云服務(wù)器中以及無法分配IP問題
答:我們使用云服務(wù)器進行代碼學(xué)習(xí)時,自己無法分配除127.0.0.1的IP,即使是自己的云服務(wù)器IP,因為是供應(yīng)商提供的虛擬IP,所以云服務(wù)器就不允許分配其他IP。
(2. IP:127.0.0.1更深層次的認(rèn)識
答:為什么可以使用127.0.0.1 IP,因為這是一個本地環(huán)回的IP,在這個IP下數(shù)據(jù)經(jīng)過本地協(xié)議棧后不會進入網(wǎng)絡(luò),而是直接從棧底回到操作系統(tǒng),同時這也是適合本地網(wǎng)絡(luò)服務(wù)測試,如果接入網(wǎng)絡(luò)通信,沒有接通則大概率是網(wǎng)絡(luò)的原因。
(3. 關(guān)于服務(wù)端bind的優(yōu)化
?答:首先我們完善一下從上面代碼對bind的認(rèn)識
?修改方法:在添加IP地址時,將IP修改為任意IP即可。
宏 : INADDR_ANY? 本質(zhì)上就是0
關(guān)于在Windows下使用客戶端
? ? ? ? 上面創(chuàng)建的客戶端都是linux下的客戶端,如果我們想在Windows下使用, -phread這個第三方庫就用不了,而客戶端就要更改源碼庫,使用window的網(wǎng)絡(luò)套接字接口,但好在幾乎類似,最后在VS本地運行即可。
自己實現(xiàn)的類一定要進行備注使用方法,返回值類型,以免代碼復(fù)用時,出現(xiàn)返回值類型問題!!(別問我為啥要單獨寫一行,因為在轉(zhuǎn)類型時,轉(zhuǎn)錯了,一直段錯誤,人都傻了,結(jié)果后面發(fā)現(xiàn),原來是自己實現(xiàn)類的用法忘記了,害,一個早上的教訓(xùn)啊)
源碼
下面的源碼介紹:cline端:基于多線程將送,收消息分離; service端:不綁定特定IP,實現(xiàn)接收,并回發(fā)消息。?
udb_socket簡單聊天室源碼:NetworkProgramming · 逆光/Linux - 碼云 - 開源中國 (gitee.com)
二,基于tcp協(xié)議編程
1. listen——服務(wù)端監(jiān)聽
?
功能:listen接口是用于創(chuàng)建一個被動的套接字,用于監(jiān)聽傳入的連接請求的接口。當(dāng)一個套接字調(diào)用listen接口后,它將開始接受傳入的連接請求,并將這些請求排隊,等待被接受或拒絕。(就像一個飯店的外面的拉客人)
sockfd:監(jiān)聽用的套接字
backlog:?指定在拒絕新連接之前,操作系統(tǒng)可以排隊等待的最大連接數(shù)量。
返回值:0成功,-1失敗。
2. accept——服務(wù)端接收
功能:accept()函數(shù)會在sockfd套接字上接受一個傳入的連接請求(阻塞式接收),并返回一個新的套接字描述符,用于和客戶端進行通信。同時,addr和addrlen參數(shù)會被填充上客戶端的地址信息。
//1. 接受請求struct sockaddr_in send_; //請求方信息bzero(&send_, sizeof send_);socklen_t len = sizeof send_;// accept會等待請求方申請,會處于阻塞狀態(tài)int actual_socket = accept(listen_socket_, (sockaddr*)&send_, &len);if ( actual_socket < 0 ){Logmessage(FATIL, "accept fail%d %s", errno, strerror(errno));continue;}
3. connect——客戶端請求
在調(diào)用 connect() 函數(shù)后,系統(tǒng)會嘗試連接到指定的服務(wù)器地址。(多客戶端向服務(wù)端進行連接)
成功,返回值為0;如果連接失敗,返回值為-1,并且可以通過 errno 變量獲取具體的錯誤信息。
// 1.保留目標(biāo)信息struct sockaddr_in goal_service;bzero(&goal_service, sizeof goal_service);goal_service.sin_family = AF_INET;goal_service.sin_port = htons(atoi(args[2]));goal_service.sin_addr.s_addr = inet_addr(args[1]);// 2.建立連接if (connect(cline_socket, (sockaddr*)&goal_service, sizeof goal_service) < 0){Logmessage(FATIL, "cline connect fail %d %s", errno, strerror(errno));exit(1);}
4. send & recev
功能:?send()函數(shù)將數(shù)據(jù)從buf緩沖區(qū)發(fā)送到已連接的套接字或者未連接的套接字(后者的UDP多用sendto)
- sockfd:要發(fā)送數(shù)據(jù)的套接字描述符。
- flags:傳遞給send()函數(shù)的標(biāo)志參數(shù),通常為0
- 返回值:成功,返回字節(jié)數(shù);失敗,-1
功能:recv()函數(shù)會阻塞程序,直到接收到足夠的數(shù)據(jù)或發(fā)生錯誤。
sockfd:指定要接收數(shù)據(jù)的套接字描述符。
flags:指定接收數(shù)據(jù)的附加選項,通常為0。
返回值:成功,字節(jié)數(shù);0,連接關(guān)閉;異常,-1。
三,基于TCP協(xié)議實現(xiàn)的線程池的服務(wù)端對客戶端進行相互通信小項目
結(jié)構(gòu)圖一覽
我的體會,客戶端以及服務(wù)端的設(shè)計,在UDP設(shè)計中基本已經(jīng)寫過了,TCP只是有一些小改動;然后就是線程池也是直接使用了,前面我們所寫的線程池小項目,總體來說考驗我們的代碼整合能力吧。
源碼?
線程池小項目:Tcp_NetworkProgramming · 逆光/Linux - 碼云 - 開源中國 (gitee.com)
有人會說,萬一我不想用線程池來實現(xiàn)服務(wù)端處理客戶端的請求呢?而是使用一些比較小型的呢?答:有,而且不止幾種
首先我們?yōu)槭裁匆镁€程池這個結(jié)構(gòu)?
答: 服務(wù)端不能一次只接受一個客戶端的請求,所以需要其他結(jié)構(gòu)(子進程或多線程)來滿足客戶端的服務(wù),主線程只要接收請求,分配任務(wù)即可。
服務(wù)端處理客戶端請求方法——由簡到密
1. 單進程處理
void start(){signal(SIGCHLD, SIG_IGN);//循環(huán)接受信息while (1){//1. 接受請求struct sockaddr_in send_; //請求方信息bzero(&send_, sizeof send_);socklen_t len = sizeof send_;// accept會等待請求方申請,會處于阻塞狀態(tài)int actual_socket = accept(listen_socket_, (sockaddr*)&send_, &len);if ( actual_socket < 0 ){Logmessage(FATIL, "accept fail%d %s", errno, strerror(errno));continue;}// 連接成功std::string send_ip = inet_ntoa(send_.sin_addr);uint16_t send_port = ntohs(send_.sin_port);Logmessage(NOWAIN, "連接成功,客戶端為 ip:%s 端口號:%d", send_ip.c_str(), send_port);// (1.0)服務(wù)器處理信息——單進程版本// //2. 分析處理數(shù)據(jù)server_dispose(actual_socket, send_ip, send_port);
? 缺點:無法滿足服務(wù)器多客戶端連接
2.?子進程處理
// 連接成功std::string send_ip = inet_ntoa(send_.sin_addr);uint16_t send_port = ntohs(send_.sin_port);Logmessage(NOWAIN, "連接成功,客戶端為 ip:%s 端口號:%d", send_ip.c_str(), send_port);// (2.0) 優(yōu)化——子進程版本pid_t pd = fork();if (pd == 0){//2. 分析處理數(shù)據(jù)close(listen_socket_); //子進程拷貝一份父進程的文件描述服表server_dispose(actual_socket, send_ip, send_port);exit(0);}close(actual_socket); // 子進程里保留了該文件描述符,父進程已經(jīng)不需要了// 按照曾經(jīng)的理解,現(xiàn)在應(yīng)該讓父進程進行等待子進程,但多少都存在些問題。// 1. waitpid阻塞式等待,不就跟單線程一樣?// 2. 非阻塞式等待,需要構(gòu)建子進程管理結(jié)構(gòu)比較麻煩,而且我們不需要關(guān)心子進程的返回情況。// 因此我們可以采用信號知識,忽略子進程返回。// 操作細則:在service啟動時 signal(SIGCHLD, SIG_IGN);
2.1.?孫子進程處理
// 連接成功std::string send_ip = inet_ntoa(send_.sin_addr);uint16_t send_port = ntohs(send_.sin_port);Logmessage(NOWAIN, "連接成功,客戶端為 ip:%s 端口號:%d", send_ip.c_str(), send_port);// (2.1) ———— 子進程退出,孫子進程讓1接管pid_t pd = fork();if (pd == 0){//2. 分析處理數(shù)據(jù)close(listen_socket_); //子進程拷貝一份父進程的文件描述服表if (fork() > 0) exit(0); // 孫子進程變成孤兒進程,讓bash接管server_dispose(actual_socket, send_ip, send_port);exit(0);}waitpid(pd, nullptr, 0); // 子進程進入立馬退出,父進程幾乎不阻塞等待
缺點:雖然滿足了服務(wù)端可以同時滿足多個客戶端連接,但是進程的創(chuàng)建會比較大的開銷 。?
3. 多線程處理
static void* pth_service(void* args){PthreadData* data = static_cast<PthreadData*>(args);// 進來先剝離線程,這樣主線程不用等待返回pthread_detach(pthread_self());server_dispose(data->_actual_socket, data->_ip, data->_port);close(data->_actual_socket);delete data;return nullptr;}..........// 連接成功std::string send_ip = inet_ntoa(send_.sin_addr);uint16_t send_port = ntohs(send_.sin_port);Logmessage(NOWAIN, "連接成功,客戶端為 ip:%s 端口號:%d", send_ip.c_str(), send_port);// (3.0) ———— 多線程版本pthread_t it = -1; // 線程的標(biāo)識號先默認(rèn)為1,后面在設(shè)置。PthreadData* data = new PthreadData;data->_ip = send_ip;data->_port = send_port;data->_actual_socket = actual_socket;pthread_create(&it, nullptr, pth_service, (void*)data);
缺點:1. 沒有設(shè)置最大線程數(shù),在高壓情況下有可能會導(dǎo)致service服務(wù)崩潰。2. 短時間內(nèi)大量請求,線程開辟消耗比較大的資源。?
4. 線程池處理
// 連接成功std::string send_ip = inet_ntoa(send_.sin_addr);uint16_t send_port = ntohs(send_.sin_port);Logmessage(NOWAIN, "連接成功,客戶端為 ip:%s 端口號:%d", send_ip.c_str(), send_port);// (4.0) ———— 啟用線程池// 讓線程來進行對網(wǎng)絡(luò)端的信息進行處理Task_add* task = new Task_add(actual_socket, send_port, send_ip);_thr_pool->push(task);// 交換策略:服務(wù)端未被占滿時,來一條就交換任務(wù)隊列if (_thr_pool->Get_queue_task_size() == 0 && _thr_pool->Get_queue_task_reserver_size() != 0){_thr_pool->swap_queue();}
功能基本上沒什么問題了,但我們在客戶端處理邏輯上是循環(huán),意味著該線程不會退出,也就是長連接。意味著,客戶端的最大連接數(shù)就是線程池的數(shù)量,如果客戶端邏輯是短連接,就不會出現(xiàn)線程池一直占滿的情況了。
以上的編程是我們在應(yīng)用層使用的編碼,往后我們將向下深入理解網(wǎng)絡(luò)理解。
下期:TCP協(xié)議原理
結(jié)語
? ?本小節(jié)就到這里了,感謝小伙伴的瀏覽,如果有什么建議,歡迎在評論區(qū)評論,如果給小伙伴帶來一些收獲請留下你的小贊,你的點贊和關(guān)注將會成為博主創(chuàng)作的動力