📢博客主頁:https://blog.csdn.net/2301_779549673
📢博客倉庫:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢歡迎點贊 👍 收藏 ?留言 📝 如有錯誤敬請指正!
📢本文由 JohnKi 原創,首發于 CSDN🙉
📢未來很長,值得我們全力奔赴更美好的生活?
文章目錄
- 🏳??🌈一、應用層
- 1.1 再談 "協議"
- 1.2 網絡版計算器
- 1.3 序列化和反序列化
- 🏳??🌈二、什么是全雙工
- 🏳??🌈三、Socket封裝
- 3.1 整體結構
- 3.2 Socket 基類
- 3.3 TcpSocket 子類
- 3.3.1 基本結構
- 3.3.2 構造、析構函數
- 3.3.3 創建套接字
- 3.3.4 綁定套接字
- 3.3.5 監聽套接字
- 3.3.6 獲取連接
- 3.3.7 建立連接
- 3.3.8 其他方法
- 3.3.10 整體代碼
- 👥總結
11111111
11111111
11111111
11111111
**** 11111111
🏳??🌈一、應用層
我們程序員寫的一個個解決我們實際問題, 滿足我們日常需求的網絡程序, 都是在應用層.
1.1 再談 “協議”
協議是一種 “約定”. socket api 的接口, 在讀寫數據時, 都是按 “字符串” 的方式來發送接收的. 如果我們要傳輸一些 “結構化的數據” 怎么辦呢?
其實,協議就是雙方約定好的結構化的數據!
1.2 網絡版計算器
例如, 我們需要實現一個服務器版的加法器. 我們需要客戶端把要計算的兩個加數發過去, 然后由服務器進行計算, 最后再把結果返回給客戶端.
約定方案一(傳結構體對象):
- 客戶端發送一個形如"1+1"的字符串;
- 這個字符串中有兩個操作數, 都是整形;
- 兩個數字之間會有一個字符是運算符, 運算符只能是 + ;
- 數字和運算符之間沒有空格;
- …
不推薦直接傳結構體對象,從技術和業務角度解釋?
1、技術角度
-
1、跨平臺與兼容性:
- 結構體的大小和內存布局可能因編譯器、操作系統或硬件平臺的不同而有所差異。這可能導致在一個平臺上發送的結構體在另一個平臺上無法正確解析。
-
2、內存對齊與填充:
- 為了優化內存訪問速度,編譯器可能會對結構體成員進行對齊和填充。這會導致結構體的實際大小大于其成員大小的總和。
- 直接傳輸結構體可能會因為內存對齊和填充的問題而導致數據解析錯誤。
-
3、指針與動態內存:
- 結構體中可能包含指針,這些指針指向動態分配的內存。直接傳輸結構體無法傳遞指針所指向的數據,而只能傳遞指針值,這可能導致數據丟失或內存泄漏。
2、業務角度
- 1、數據安全性:
- 直接傳輸結構體可能會暴露數據的內部結構和實現細節,從而增加數據被惡意攻擊的風險。
- 2、數據版本控制:
- 隨著業務的發展和變化,數據結構和格式可能需要進行調整和升級。
約定方案二(傳字符串):
- 定義結構體來表示我們需要交互的信息;
- 發送數據時將這個結構體按照一個規則轉換成字符串, 接收到數據的時候再按照相同的規則把字符串轉化回結構體;
- 這個過程叫做 “
序列化
” 和 “反序列化
”
1.3 序列化和反序列化
無論我們采用方案一, 還是方案二, 還是其他的方案, 只要保證, 一端發送時構造的數據,在另一端能夠正確的進行解析, 就是 ok 的. 這種約定, 就是 應用層協議
但是,為了讓我們深刻理解協議,我們打算自定義實現一下協議的過程。
- 我們采用方案 2,我們也要體現協議定制的細節
- 我們要引入序列化和反序列化,只不過我們課堂直接采用現成的方案 – jsoncpp庫
- 我們要對
socket
進行字節流的讀取處理
🏳??🌈二、什么是全雙工
所以:
- 在任何一臺主機上,TCP 連接既有發送緩沖區,又有接受緩沖區,所以,在內核中,可以在發消息的同時,也可以收消息,即全雙工
- 這就是為什么一個 tcp sockfd 讀寫都是它的原因
- 實際數據什么時候發,發多少,出錯了怎么辦,由 TCP 控制,所以 TCP 叫做傳輸控制協議
1、read,write,send,recv本質是拷貝函數!
2、發送數據的本質:是從發送方的發送緩沖區把數據通過協議棧和網絡拷貝給接收方大的接收緩沖區!
3、tcp支持全雙工通信的原因(有發送和接收緩沖區)!
4、有兩個緩沖區這種模式就是生產者消費者模型!
5、為什么IO函數要阻塞?本質是在維護同步關系!
TCP協議是面向字節流的,客戶端發的,不一定是全部是服務端收的,怎么保證讀到的是一個完整的請求呢?
- 分割完整的報文!
🏳??🌈三、Socket封裝
Socket類以模板方法類的設計模式進行封裝,將算法的不變部分封裝在抽象基類中 ,而將可變部分延遲到子類中實現!
3.1 整體結構
-
抽象類(Abstract Class):
- 定義了多個抽象操作,這些操作在抽象類中不具體實現,由子類實現。
- 定義了兩個模板方法,這個方法通常調用了上面提到的抽象操作。模板方法的算法骨架是固定的,但其中一些步驟的具體實現會延遲到子類中。
-
具體子類(Concrete Class):
- 實現抽象類中的抽象操作,提供具體的算法步驟實現。
- 可以重寫父類中的模板方法,但通常情況下不需要這么做,因為模板方法已經在抽象類中定義好了算法的骨架。
3.2 Socket 基類
將套接字創建、綁定、監聽等 ?通用流程? 抽象為模板方法,如 BuildListenSocket,而將具體步驟(如 CreaterSocketOrDie)延遲到子類實現。
?角色劃分?:
- ?抽象類(Abstract Class)??:Socket 定義了純虛函數(步驟方法)和模板方法(流程框架)。?
- 具體子類(Concrete Class)??:由用戶繼承 Socket 并實現純虛函數(例如 TcpSocket、UdpSocket)。
using SockPtr = std::shared_ptr<Socket>;class Socket {public:virtual void CreateSocketOrDie() = 0; // 創建套接字virtual void BindOrDie(uint16_t port) = 0; // 綁定套接字virtual void ListenOrDie(int backlog = gbacklog) = 0; // 監聽套接字virtual SockPtr Accepter(InetAddr* cli) = 0; // 獲取鏈接virtual bool Connector(const std::string& peerip,uint16_t peerport) = 0; // 簡歷連接virtual int Sockfd() = 0;virtual void Close() = 0;virtual ssize_t Recv(std::string* out) = 0; // 接收數據virtual ssize_t Send(const std::string& in) = 0; // 發送數據public:// 創建監聽套接字void BuildListenSocket(uint16_t port) {CreateSocketOrDie(); // 創建BindOrDie(port); // 綁定ListenOrDie(); // 監聽}// 創建客戶端套接字void BuildConnectorSocket(const std::string& peerip, uint16_t peerport) {CreateSocketOrDie(); // 創建Connector(peerip, peerport); // 連接}
};
3.3 TcpSocket 子類
TcpSocket類
繼承Socket類
,并具體實現父類的抽象操作!
3.3.1 基本結構
TcpSocket 子類就是具體實現父類的抽象操作,所以所有 TCP 可能會用到的父類方法都要具體實現
class TcpSocket : public Socket {
public:TcpSocket() {}TcpSocket(int sockfd) {}// 創建套接字void CreateSocketOrDie() {}// 綁定套接字void BindOrDie(uint16_t port) {}// 監聽套接字void ListenOrDie(int backlog = gbacklog) {}// 獲取鏈接SockPtr Accepter(InetAddr* cli) {}// 建立連接bool Connector(const std::string& peerip, uint16_t peerport) {}// 獲取套接字描述符int Sockfd() {}// 關閉套接字void Close() {}~TcpSocket() {}private:int _sockfd;
};
3.3.2 構造、析構函數
- 構造函數 可以實現兩個,一個無參構造,一個有參構造(傳參sockfd),用于初始化成員變量
- 析構函數 可以不做處理,后面關閉套接字自己調用關閉函數即可!
TcpSocket() {}
TcpSocket(int sockfd) : _sockfd(sockfd) {}
~TcpSocket() {}
3.3.3 創建套接字
使用庫函數 socket 按照格式創建套接字
// 創建套接字
void CreateSocketOrDie() override {_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0) {LOG(LogLevel::ERROR) << "create socket error";exit(SOCKET_ERR);}LOG(LogLevel::DEBUG) << "sockfd create success : " << _sockfd;
}
3.3.4 綁定套接字
這部分用于服務端,所以需要先構建服務端的網絡字節序地址,然后綁定套接字和網絡字節序地址
// 綁定套接字void BindOrDie(uint16_t port) override{// sockaddr_in 的頭文件是 #include <netinet/in.h>struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = htonl(INADDR_ANY);int n = ::bind(_sockfd, CONV(&local), sizeof(local));if(n < 0){LOG(LogLevel::ERROR) << "bind socket error";exit(BIND_ERR);}LOG(LogLevel::DEBUG) << "bind success";}
3.3.5 監聽套接字
在綁定好網絡字節序地址后,我們需要形成老板模式,設置最大連接數量,然后不斷監聽
// 監聽套接字
void ListenOrDie(int backlog = gbacklog) override {int n = ::listen(_sockfd, backlog);if (n < 0) {LOG(LogLevel::ERROR) << "listen socket error";exit(LISTEN_ERR);}LOG(LogLevel::DEBUG) << "listen success";
}
3.3.6 獲取連接
在為監聽套接字設置好最大連接長度后,我們不斷使用 accept 監聽這個套接字,將獲取到的客戶端ip和端口號等信息 與 我們的服務端的連接 整合起來,形成一個整體套接字,實現面向對象連接
// 獲取鏈接
SockPtr Accepter(InetAddr* cli) override {struct sockaddr_in client;socklen_t clientlen = sizeof(client);// accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)// 返回一個新的套接字,該套接字與調用進程間接地建立了連接。int sockfd = ::accept(_sockfd, CONV(&client), &clientlen);if (sockfd < 0) {LOG(LogLevel::ERROR) << "accept socket error";return nullptr;}*cli = InetAddr(client);LOG(LogLevel::DEBUG) << "get a new connection from "<< cli->AddrStr().c_str() << ", sockfd : " << sockfd;return std::make_shared<TcpSocket>(sockfd);
}
3.3.7 建立連接
當客戶端想要連接上服務端的時候,我們需要先為服務端創建一個網絡字節序地址,再與客戶端的套接字連接起來
// 建立連接
bool Connector(const std::string& serverip, uint16_t serverport) override {struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET; // IPv4協議server.sin_port = htons(serverport); // 端口號// 這句話表示將字符串形式的IP地址轉換為網絡字節序的IP地址// inet_pton函數的作用是將點分十進制的IP地址轉換為網絡字節序的IP地址// 這里的AF_INET表示IPv4協議// 這里的serverip.c_str()表示IP地址的字符串形式// &server.sin_addr表示將IP地址存儲到sin_addr成員變量中::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // IP地址int n = ::connect(_sockfd, CONV(&server), sizeof(server));if (n < 0) {LOG(LogLevel::ERROR) << "connect socket error";return false;}LOG(LogLevel::DEBUG) << "connect success";return true;
}
3.3.8 其他方法
還有一些其他的方法,難度不大,就不一一介紹了
// 獲取套接字描述符
int Sockfd() override { return _sockfd; }// 關閉套接字
void Close() override {if (_sockfd >= 0)::close(_sockfd);
}// 接收數據
ssize_t Recv(std::string* out) override {char inbuffer[4096];ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0) {inbuffer[n] = 0;*out = inbuffer;}return n;
}// 發送數據
ssize_t Send(const std::string& in) override {return ::send(_sockfd, in.c_str(), in.size(), 0);
}
3.3.10 整體代碼
#pragma once #include <iostream>
#include <cstring>
#include <Socket.h>
#include <memory>#include <netinet/in.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const int gbacklog = 8;// common.hpp
// #define Die(code) \
// do \
// { \
// exit(code); \
// } while (0)// #define CONV(v) (struct sockaddr *)(v)// enum
// {
// USAGE_ERR = 1,
// SOCKET_ERR,
// BIND_ERR,
// LISTEN_ERR,
// CONNECTION_ERR
// };namespace SocketModule{using SockPtr = std::shared_ptr<Socket>;class Socket{public:virtual void CreateSocketOrDie() = 0; // 創建套接字virtual void BindOrDie(uint16_t port) = 0; // 綁定套接字virtual void ListenOrDie(int backlog = gbacklog) = 0; // 監聽套接字virtual SockPtr Accepter(InetAddr* cli) = 0; // 獲取鏈接virtual bool Connector(const std::string& serverip, uint16_t serverport) = 0; // 簡歷連接virtual int Sockfd() = 0;virtual void Close() = 0;virtual ssize_t Recv(std::string* out) = 0; // 接收數據virtual ssize_t Send(const std::string& in) = 0; // 發送數據 public:// 創建監聽套接字void BuildListenSocket(uint16_t port){CreateSocketOrDie(); // 創建BindOrDie(port); // 綁定ListenOrDie(); // 監聽}// 創建客戶端套接字void BuildConnectorSocket(const std::string& serverip, uint16_t serverport){CreateSocketOrDie(); // 創建Connector(serverip, serverport); // 連接}};class TcpSocket : public Socket{public:TcpSocket(){}TcpSocket(int sockfd) : _sockfd(sockfd){ }// 創建套接字void CreateSocketOrDie() override{_sockfd = socket(AF_INET, SOCK_STREAM, 0);if(_sockfd < 0){LOG(LogLevel::ERROR) << "create socket error";exit(SOCKET_ERR);}LOG(LogLevel::DEBUG) << "sockfd create success : " << _sockfd;}// 綁定套接字void BindOrDie(uint16_t port) override{// sockaddr_in 的頭文件是 #include <netinet/in.h>struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = htonl(INADDR_ANY);int n = ::bind(_sockfd, CONV(&local), sizeof(local));if(n < 0){LOG(LogLevel::ERROR) << "bind socket error";exit(BIND_ERR);}LOG(LogLevel::DEBUG) << "bind success";} // 監聽套接字void ListenOrDie(int backlog = gbacklog) override{int n = ::listen(_sockfd, backlog);if(n < 0){LOG(LogLevel::ERROR) << "listen socket error";exit(LISTEN_ERR);}LOG(LogLevel::DEBUG) << "listen success";}// 獲取鏈接SockPtr Accepter(InetAddr* cli) override{struct sockaddr_in client;socklen_t clientlen = sizeof(client);// accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)// 返回一個新的套接字,該套接字與調用進程間接地建立了連接。int sockfd = ::accept(_sockfd, CONV(&client), &clientlen);if(sockfd < 0){LOG(LogLevel::ERROR) << "accept socket error";return nullptr;}*cli = InetAddr(client);LOG(LogLevel::DEBUG) << "get a new connection from " << cli->AddrStr().c_str() << ", sockfd : " << sockfd;return std::make_shared<TcpSocket>(sockfd);}// 建立連接bool Connector(const std::string& serverip, uint16_t serverport) override{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET; // IPv4協議server.sin_port = htons(serverport); // 端口號// 這句話表示將字符串形式的IP地址轉換為網絡字節序的IP地址// inet_pton函數的作用是將點分十進制的IP地址轉換為網絡字節序的IP地址// 這里的AF_INET表示IPv4協議// 這里的serverip.c_str()表示IP地址的字符串形式// &server.sin_addr表示將IP地址存儲到sin_addr成員變量中::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // IP地址int n = ::connect(_sockfd, CONV(&server), sizeof(server));if(n < 0){LOG(LogLevel::ERROR) << "connect socket error" ;return false;}LOG(LogLevel::DEBUG) << "connect success";return true;}// 獲取套接字描述符int Sockfd() override{ return _sockfd; }// 關閉套接字void Close() override{ if(_sockfd >= 0) ::close(_sockfd); }// 接收數據ssize_t Recv(std::string* out) override{char inbuffer[4096];ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if(n > 0){inbuffer[n] = 0;*out = inbuffer;}return n;} // 發送數據 ssize_t Send(const std::string& in) override{return ::send(_sockfd, in.c_str(), in.size(), 0);}~TcpSocket(){}private:int _sockfd;};
}
👥總結
本篇博文對 【Linux網絡】應用層自定義協議與序列化及Socket模擬封裝 做了一個較為詳細的介紹,不知道對你有沒有幫助呢
覺得博主寫得還不錯的三連支持下吧!會繼續努力的~