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

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

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

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

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