目錄
4-1 應用層
4-2 重新理解 read、write、recv、send 和 tcp 為什么支持全雙工
4-3 開始實現
4-1 應用層
我們程序員寫的一個個解決我們實際問題 , 滿足我們日常需求的網絡程序 , 都是在應用
層 .
再談 " 協議 "
協議是一種 " 約定 ". socket api 的接口 , 在讀寫數據時 , 都是按 " 字符串 " 的方式來發送接
收的 . 如果我們要傳輸一些 " 結構化的數據 " 怎么辦呢 ?
其實,協議就是雙方約定好的結構化的數據
網絡版計算器
例如 , 我們需要實現一個服務器版的加法器 . 我們需要客戶端把要計算的兩個加數發過
去 , 然后由服務器進行計算 , 最后再把結果返回給客戶端 .
約定方案一 :
?
客戶端發送一個形如 "1+1" 的字符串 ;
?
這個字符串中有兩個操作數 , 都是整形 ;
?
兩個數字之間會有一個字符是運算符 , 運算符只能是 + ;
?
數字和運算符之間沒有空格 ;
?
...
約定方案二 :
?
定義結構體來表示我們需要交互的信息 ;
?
發送數據時將這個結構體按照一個規則轉換成字符串 , 接收到數據的時候再按
照相同的規則把字符串轉化回結構體 ;
?
這個過程叫做 " 序列化 " 和 " 反序列化 "
序列化 和 反序列化

無論我們采用方案一 , 還是方案二 , 還是其他的方案 , 只要保證 , 一端發送時構造的數據 ,
在另一端能夠正確的進行解析 , 就是 ok 的 . 這種約定 , 就是 應用層協議
但是,為了讓我們深刻理解協議,我們打算自定義實現一下協議的過程。
?
我們采用方案 2 ,我們也要體現協議定制的細節
?
我們要引入序列化和反序列化,只不過我們課堂直接采用現成的方案 -- jsoncpp
庫
?
我們要對 socket 進行字節流的讀取處理
4-2 重新理解 read、write、recv、send 和 tcp 為什么支持全雙工
在任何一臺主機上, TCP 連接既有發送緩沖區,又有接受緩沖區,所以,在內核
中,可以在發消息的同時,也可以收消息,即全雙工
?
這就是為什么一個 tcp sockfd 讀寫都是它的原因
實際數據什么時候發,發多少,出錯了怎么辦,由 TCP 控制,所以 TCP 叫做傳輸控制協議
4-3 開始實現
代碼結構
C++
Calculate.hpp Makefile Socket.hpp TcpServer.hpp
Daemon.hpp Protocol.hpp TcpClientMain.cc TcpServerMain.cc
// 簡單起見,可以直接采用自定義線程
// 為了減少課堂時間的浪費,也建議不用用戶輸入,直接 client<<->>server 通
信,這樣可以省去編寫沒有干貨的代碼
Socket 封裝
socket.hpp
C++
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define Convert(addrptr) ((struct sockaddr *)addrptr)
namespace Net_Work
{
const static int defaultsockfd = -1;
const int backlog = 5;
enum
{
SocketError = 1,
BindError,
ListenError,
};
// 封裝一個基類,Socket 接口類
// 設計模式:模版方法類
class Socket
{
public:
virtual ~Socket() {}
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie(int backlog) = 0;
virtual Socket *AcceptConnection(std::string *peerip,
uint16_t *peerport) = 0;
virtual bool ConnectServer(std::string &serverip, uint16_t
serverport) = 0;
virtual int GetSockFd() = 0;
virtual void SetSockFd(int sockfd) = 0;
virtual void CloseSocket() = 0;
virtual bool Recv(std::string *buffer, int size) = 0;
virtual void Send(std::string &send_str) = 0;
// TODO
public:
void BuildListenSocketMethod(uint16_t port, int backlog)
{
CreateSocketOrDie();
BindSocketOrDie(port);
ListenSocketOrDie(backlog);
}
bool BuildConnectSocketMethod(std::string &serverip,
uint16_t serverport)
{
CreateSocketOrDie();
return ConnectServer(serverip, serverport);
}
void BuildNormalSocketMethod(int sockfd)
{
SetSockFd(sockfd);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd)
{
}
~TcpSocket()
{
}
void CreateSocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
exit(SocketError);
}
void BindSocketOrDie(uint16_t port) override
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
int n = ::bind(_sockfd, Convert(&local),
sizeof(local));
if (n < 0)
exit(BindError);
}
void ListenSocketOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
exit(ListenError);
}
Socket *AcceptConnection(std::string *peerip, uint16_t
*peerport) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newsockfd = ::accept(_sockfd, Convert(&peer),
&len);
if (newsockfd < 0)
return nullptr;
*peerport = ntohs(peer.sin_port);
*peerip = inet_ntoa(peer.sin_addr);
Socket *s = new TcpSocket(newsockfd);
return s;
}
bool ConnectServer(std::string &serverip, uint16_t
serverport) override
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_port = htons(serverport);
int n = ::connect(_sockfd, Convert(&server),
sizeof(server));
if (n == 0)
return true;
else
return false;
}
int GetSockFd() override
{
return _sockfd;
}
void SetSockFd(int sockfd) override
{
_sockfd = sockfd;
}
void CloseSocket() override
{
if (_sockfd > defaultsockfd)
::close(_sockfd);
}
bool Recv(std::string *buffer, int size) override
{
char inbuffer[size];
ssize_t n = recv(_sockfd, inbuffer, size-1, 0);
if(n > 0)
{
inbuffer[n] = 0;
*buffer += inbuffer; // 故意拼接的
return true;
}
else if(n == 0) return false;
else return false;
}
void Send(std::string &send_str) override
{
// 多路轉接我們在統一說
send(_sockfd, send_str.c_str(), send_str.size(), 0);
}
private:
int _sockfd;
};
}
定制協議
基本結構
?
定制基本的結構化字段,這個就是協議
C++
class Request
{
private:
// _data_x _oper _data_y
// 報文的自描述字段
// "len\r\nx op y\r\n" : \r\n 不屬于報文的一部分,約定
// 很多工作都是在做字符串處理!
int _data_x; // 第一個參數
int _data_y; // 第二個參數
char _oper; // + - * / %
};
class Response
{
private:
// "len\r\n_result _code\r\n"
int _result; // 運算結果
int _code; // 運算狀態
};
protocol.hpp
C++
#pragma once
#include <iostream>#include <memory>
#include <jsoncpp/json/json.h>
namespace Protocol
{
// 問題
// 1. 結構化數據的序列和反序列化
// 2. 還要解決用戶區分報文邊界 --- 數據包粘報問題
// 講法
// 1. 自定義協議
// 2. 成熟方案序列和反序列化
// 總結:
// 我們今天定義了幾組協議呢??我們可以同時存在多個協議嗎???可以
// "protocol_code\r\nlen\r\nx op y\r\n" : \r\n 不屬于報文的一部
分,約定
const std::string ProtSep = " ";
const std::string LineBreakSep = "\r\n";
// "len\r\nx op y\r\n" : \r\n 不屬于報文的一部分,約定
std::string Encode(const std::string &message)
{
std::string len = std::to_string(message.size());
std::string package = len + LineBreakSep + message +
LineBreakSep;
return package;
}
// "len\nx op y\n" : \n 不屬于報文的一部分,約定
// 我無法保證 package 就是一個獨立的完整的報文
// "l
// "len
// "len\r\n
// "len\r\nx
// "len\r\nx op
// "len\r\nx op y
// "len\r\nx op y\r\n"
// "len\r\nx op y\r\n""len
// "len\r\nx op y\r\n""len\n
// "len\r\nx op
// "len\r\nx op y\r\n""len\nx op y\r\n"
// "len\r\nresult code\r\n""len\nresult code\r\n"
bool Decode(std::string &package, std::string *message)
{
// 除了解包,我還想判斷報文的完整性, 能否正確處理具有"邊界"的報
文
auto pos = package.find(LineBreakSep);
if (pos == std::string::npos)
return false;
std::string lens = package.substr(0, pos);
int messagelen = std::stoi(lens);
int total = lens.size() + messagelen + 2 *
LineBreakSep.size();
if (package.size() < total)
return false;
// 至少 package 內部一定有一個完整的報文了!
*message = package.substr(pos + LineBreakSep.size(),
messagelen);
package.erase(0, total);
return true;
}
class Request
{
public:
Request() : _data_x(0), _data_y(0), _oper(0)
{
}
Request(int x, int y, char op) : _data_x(x), _data_y(y),
_oper(op)
{
}
void Debug()
{
std::cout << "_data_x: " << _data_x << std::endl;
std::cout << "_data_y: " << _data_y << std::endl;
std::cout << "_oper: " << _oper << std::endl;
}
void Inc()
{
_data_x++;
_data_y++;
}
// 結構化數據->字符串
bool Serialize(std::string *out)
{
Json::Value root;
root["datax"] = _data_x;
root["datay"] = _data_y;
root["oper"] = _oper;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) // "x op y" [)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
if(res)
{
_data_x = root["datax"].asInt();
_data_y = root["datay"].asInt();
_oper = root["oper"].asInt();
}
return res;
}
int GetX() { return _data_x; }
int GetY() { return _data_y; }
char GetOper() { return _oper; }
private:
// _data_x _oper _data_y
// 報文的自描述字段
// "len\r\nx op y\r\n" : \r\n 不屬于報文的一部分,約定
// 很多工作都是在做字符串處理!
int _data_x; // 第一個參數
int _data_y; // 第二個參數
char _oper; // + - * / %
};
class Response
{
public:
Response() : _result(0), _code(0)
{
}
Response(int result, int code) : _result(result),
_code(code)
{
}
bool Serialize(std::string *out)
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) // "_result _code" [)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
if(res)
{
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return res;
}
void SetResult(int res) { _result = res; }
void SetCode(int code) { _code = code; }
int GetResult() { return _result; }
int GetCode() { return _code; }
private:
// "len\r\n_result _code\r\n"
int _result; // 運算結果
int _code; // 運算狀態
};
// 簡單的工廠模式,建造類設計模式
class Factory
{
public:
std::shared_ptr<Request> BuildRequest()
{
std::shared_ptr<Request> req =
std::make_shared<Request>();
return req;
}
std::shared_ptr<Request> BuildRequest(int x, int y, char
op)
{
std::shared_ptr<Request> req =
std::make_shared<Request>(x, y, op);
return req;
}
std::shared_ptr<Response> BuildResponse()
{
std::shared_ptr<Response> resp =
std::make_shared<Response>();
return resp;
}
std::shared_ptr<Response> BuildResponse(int result, int
code)
{
std::shared_ptr<Response> req =
std::make_shared<Response>(result, code);
return req;
}
};
}
4-4 關于流式數據的處理
?
你如何保證你每次讀取就能讀完請求緩沖區的所有內容?
?
你怎么保證讀取完畢或者讀取沒有完畢的時候,讀到的就是一個完整的請求呢?
?
處理 TCP 緩沖區中的數據,一定要保證正確處理請求
C++
const std::string ProtSep = " ";
const std::string LineBreakSep = "\n";
// "len\nx op y\n" : \n 不屬于報文的一部分,約定
std::string Encode(const std::string &message)
{
std::string len = std::to_string(message.size());
std::string package = len + LineBreakSep + message +
LineBreakSep;
return package;
}
// "len\nx op y\n" : \n 不屬于報文的一部分,約定
// 我無法保證 package 就是一個獨立的完整的報文
// "l
// "len
// "len\n
// "len\nx
// "len\nx op
// "len\nx op y
// "len\nx op y\n"
// "len\nx op y\n""len
// "len\nx op y\n""len\n
// "len\nx op
// "len\nx op y\n""len\nx op y\n"
// "len\nresult code\n""len\nresult code\n"
bool Decode(std::string &package, std::string *message)
{
// 除了解包,我還想判斷報文的完整性, 能否正確處理具有"邊界"的報
文
auto pos = package.find(LineBreakSep);
if (pos == std::string::npos)
return false;
std::string lens = package.substr(0, pos);
int messagelen = std::stoi(lens);
int total = lens.size() + messagelen + 2 *
LineBreakSep.size();
if (package.size() < total)
return false;
// 至少 package 內部一定有一個完整的報文了!
*message = package.substr(pos + LineBreakSep.size(),
messagelen);
package.erase(0, total);
return true;