一、简单了解TCP协议(引子)
1.1 三次握手
三次握手就是客户端向服务端发起连接的过程
服务器初始化
-
调用socket,创建套接字文件
-
调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了,就会bind失败
-
调用listen,将套接字文件设置为监听状态,为后面的accept做好准备
-
调用accept,阻塞等待客户端的链接请求
客户端发起连接
-
调用socket,创建套接字文件
-
调用connect,向服务器发起连接请求:
-
connect会发出SYN并阻塞等待服务器应答 (第一次)
-
服务器收到客户端的SYN,会应答一个SYN+ACK段表示"同意建立连接" (第二次)
-
客户端收到SYN+ACK后会从connect返回,同时应答一个ACK段 (第三次)
-
最后服务端收到ACK段后从accept返回,建立连接成功。
-
这个建立连接的过程, 通常称为“三次握手”
1.2 数据传输
双方建立好连接之后就可以进行数据传输了
TCP协议提供全双工通信服务
- tcp是全双工通信,因为每个链接(每个套接字文件)都有对应的发送和接收缓冲区,收发可以并发执行(但是多线程单独收或发必须串行执行)
- read, write只负责应用层和传输层缓冲区之间数据的拷贝,而传输层缓冲区和网络之间数据的收发(发多少,何时发)则完全是由tcp协议负责,所以TCP协议称为传输控制协议。
TCP是面向字节流协议
- 当tcp在传输层发送消息时,一个tcp报文可能会被分割成多条消息转发给网络层。我们不能认为一条消息就是一个tcp报文,所以tcp是面向字节流的协议
- 由于一条消息对应的不是一个tcp报文,如果接受方不知道一个tcp报文的长度或者分割的边界在哪里,就会无法组装成一个单独完整的报文,这就形成了粘包问题
- TCP协议只负责适时收发缓冲区中的数据,至于数据包是否完整(可能只发送了一部分),是否是单独的数据包(可能一次读到多份数据),数据包内容的解析等则需要由应用层协议负责,这些内容都会在后面的自定义协议(应用层)当中体现。
1.3 四次挥手
如果不想通信了,双方就要断开连接
断开连接的过程
-
如果客户端没有更多的请求了,就调用close关闭连接, 客户端会向服务器发送FIN段 (第一次)
-
此时服务器收到FIN后, 会回应一个ACK,同时read会返回0 (第二次)
-
read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN (第三次)
-
客户端收到FIN,再返回一个ACK给服务器 (第四次)
这个断开连接的过程, 通常称为 ”四次挥手“
二、传输结构化数据
2.1 当前存在的问题
在之前的socket编程当中, 读写数据时都是按 “字符串” 的方式来发送接收的,如果我们要传输一些"结构化的数据" 怎么办呢?
什么是结构化的数据?
比如QQ聊天,就不能单纯发送消息过去,还要把头像url、时间昵称等打包形成一个报文,把这个报文的数据一起发送给对方,这个打包形成的报文就是一个结构化的数据
能不能将将结构体直接发送给对方呢?
不可以,不同的编程语言和编译环境对同一个结构体的描述可能不同,如内存对齐规则和数据的大小端模式等。如果选择直接收发二进制形式的结构体数据,可能会导致数据读写不一致的问题。
如何保证读取到完整、单独的数据?
在前面TCP协议的数据传输部分,我们就提到了这个问题,解决该问题的方法就是明确报文的大小和边界:
- 对报文进行定长
- 用特殊符号划分每条报文
- 自描述方式
2.2 序列化和反序列化
什么是序列化和反序列化?
- 序列化是将数据结构或对象转换为字节流的过程。在序列化过程中,对象的状态信息被转换为字节序列,可以将其存储在文件中或通过网络传输
- 反序列化是将字节流或其他存储形式转换回数据结构或对象的过程。在反序列化过程中,字节序列被重新转换为对象的状态信息,以便可以重新创建对象并使用其数据
序列化和反序列化的目的
- 数据持久化:通过序列化,可以将对象的状态保存到文件或数据库中,以便在程序重新启动或重新加载时可以从中恢复对象的状态
- 数据传输:通过序列化,可以将对象转换为字节流,以便在网络传输中进行传递
- 跨平台和跨语言交互:通过序列化,可以将对象转换为通用的字节流格式,使得不同平台和不同编程语言之间可以进行数据交换和共享。无论是Java、Python、C++还是其他编程语言,只要能够进行序列化和反序列化操作,就可以实现跨平台和跨语言的数据交互
发送报文到网络时候,报文首先需要进行序列化,然后再发送,报文通过协议栈发送给对方后,接收报文的一方也需要对报文进行反序列化,才能正常使用该报文
2.3 为报文内容添加报头
我们将序列化后的结构化数据称为报文内容,为了保证能够读取到完整、单独的数据,我们需要:
- 用特殊符号划分每条报文:在报文和报文之间添加换行符进行划分(方便后期调试,也可以用其他符号)
- 自描述:为报文内容添加报头,其中可以包含协议类型、报文内容的长度等信息。
报头中包含协议类型:可以使用多态技术将应用层协议拓展出多种数据类型以适应各种应用需求。此时就需要报头中包含协议类型,使通信双方使用同一子类型协议。
报头中包含报文内容的长度:保证读取到完整、单独的数据
对比UDP协议
- UDP协议的特点是面向数据包传输,系统会自动进行数据的格式处理,将数据和数据分开。发送几次就需要接收几次。
- TCP协议的特点是面向字节流传输,系统不会对数据进行分包,需要用户自己在应用层定制协议进行分包和解包操作。
三、网络计算器程序
3.1 封装socket API
首先我们要对socket API进行封装,方便后续客户端和服务端使用socket API进行网络通信。
#pragma once#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include "logmessage.hpp"class Socket
{int _sockfd;static const int s_backlog = 20;public:Socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd == -1){LogMessage(FATAL, "socket(%d): %s", errno, strerror(errno));exit(errno);}LogMessage(DEBUG, "create socket success!");}void Bind(const std::string &ip, uint16_t port){sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = inet_addr(ip.c_str());if (bind(_sockfd, (sockaddr *)&local, sizeof(local)) == -1){LogMessage(FATAL, "bind(%d): %s", errno, strerror(errno));exit(errno);}LogMessage(DEBUG, "bind socket success!");}void Listen(){if (listen(_sockfd, s_backlog)){LogMessage(FATAL, "listen(%d): %s", errno, strerror(errno));exit(errno);}LogMessage(DEBUG, "listen socket success!");}int Accept(std::string *ip, uint16_t *port){sockaddr_in src;socklen_t len;int svcsock = accept(_sockfd, (sockaddr *)&src, &len);if (svcsock == -1){LogMessage(ERROR, "accept(%d): %s", errno, strerror(errno));return -1;}if (ip != nullptr)*ip = inet_ntoa(src.sin_addr);if (port != nullptr)*port = ntohs(src.sin_port);return svcsock;}void Connect(const std::string &ip, uint16_t port){sockaddr_in dst;memset(&dst, 0, sizeof(dst));dst.sin_family = AF_INET;dst.sin_port = htons(port);dst.sin_addr.s_addr = inet_addr(ip.c_str());if (connect(_sockfd, (sockaddr *)&dst, sizeof(dst)) == -1){LogMessage(FATAL, "connect(%d): %s", errno, strerror(errno));exit(errno);}}int getsock(){return _sockfd;}~Socket(){if (_sockfd >= 0){close(_sockfd);}}
};
3.2 自定义协议
定制的协议,必须保证通信双方(客户端、服务端)能够遵守协议的约定。
应用层协议需要解决的问题:
- 定义结构化数据:定义通信双方都能够解释的结构化数据,数据可以分为请求数据(request)和响应数据(response),因此我们分别需要对请求数据和响应数据进行约定,可以用两个结构体进行封装数据的请求和响应
- 数据格式转换:序列化和反序列化,正确的传输结构化数据
- 分包和解包:为报文内容添加报头,保证读取到完整、单独的数据
提示:客户端和服务端要包含同一份协议代码,这样才能保证双方使用的是同一个协议。
protocol.hpp
#pragma once
#include <sstream>#define SEP "|"
#define SPACE " "struct Request
{int _l;int _r;char _op;Request() {}Request(int l, int r, char op): _l(l), _r(r), _op(op){}//序列化std::string Serialize(){std::stringstream oss;oss << _l << SPACE << _op << SPACE << _r;return oss.str();}//反序列化bool Deserialize(const std::string &str){std::size_t left = str.find(SPACE);if (left == std::string::npos)return false;std::size_t right = str.rfind(SPACE);if (right == std::string::npos)return false;_l = std::stoi(str.substr(0, left));_r = std::stoi(str.substr(right + strlen(SPACE)));if (left + strlen(SPACE) > str.size())return false;else_op = str[left + strlen(SPACE)];return true;}
};struct Response
{int _ret;int _code;Response() {}Response(int ret, int code): _ret(ret), _code(code){}//序列化std::string Serialize(){std::stringstream oss;oss << _ret << SPACE << _code;return oss.str();}//反序列化bool Deserialize(const std::string &str){std::size_t pos = str.find(SPACE);if (pos == std::string::npos)return false;_ret = std::stoi(str.substr(0, pos));_code = std::stoi(str.substr(pos + strlen(SPACE)));return true;}
};//报头的添加和解析是以上两个结构通用的,所以定义成全局函数。
//解析报头
std::string Decode(std::string &buffer)
{size_t pos = buffer.find(SEP);if (pos == std::string::npos)return "";int len = std::stoi(buffer.substr(0, pos)); //报头中的报文内容长度int content_len = buffer.size() - pos - 2 * strlen(SEP); //减去报头和2个sep的剩余长度if (content_len >= len){//报文完整,获取报文内容,将完成解析的报文从缓冲区中去除buffer.erase(0, pos+strlen(SEP));std::string package;package = buffer.substr(0, len);buffer.erase(0, len+strlen(SEP));return package;}else{//报文不完整,返回空串return ""; }
}//添加报头和报文间分隔符
std::string Encode(const std::string &str)
{std::stringstream oss;oss << std::to_string(str.size()) << SEP << str << SEP;return oss.str();
}//也封装以下读写函数
bool Receive(int sock, std::string *out)
{char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer), 0);if (s > 0){buffer[s] = 0;*out += buffer;return true;}else{return false;}
}void Send(int sock, const std::string &str)
{send(sock, str.c_str(), str.size(), 0);
}
3.2.1 send、recv函数
send函数,用于TCP发送数据
函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
- sockfd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
- buf:需要发送的数据。
- len:需要发送数据的字节个数。
- flags:发送的方式,一般设置为0,表示阻塞式发送。
返回值说明:
- 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
提示:该函数与write函数功能一致
recv函数,用于TCP接收数据
函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
- sockfd:特定的文件描述符,表示从该文件描述符中读取数据。
- buf:数据的存储位置,表示将读取到的数据存储到该位置。
- len:数据的个数,表示从该文件描述符中读取数据的字节数。
- flags:读取的方式,一般设置为0,表示阻塞式读取。
返回值说明:
- 如果返回值大于0,则表示本次实际读取到的字节个数。
- 如果返回值等于0,则表示对端已经把连接关闭了。
- 如果返回值小于0,则表示读取时遇到了错误。
提示:该函数的功能与read一致
3.2.2 使用json库进行数据格式交换
我们一般不用自己编写序列和反序列化,因为有现成的库支持像这样的数据格式交换。
常见的序列化和反序列化的库:
-
json
-
protobuf
-
xml
其中,json 是简单易上手的,C++、Java,Python等都支持,protobuf 和 json 是C++常用的
安装json库
yum install -y jsoncpp-devel
注意:普通用户需要 sudo 提权
安装完成,在系统默认路径下查看头文件和库文件
使用json需要包含头文件
#include <jsoncpp/json/json.h>
编译时需要指明链接jsoncpp库
使用json库进行数据格式交换
序列化
- 定义
Json::Value
对象,可以嵌套定义 - 使用
value["x"] = n
的方式添加键值对 - 定义
Json::StyledWriter
(序列化结果方便查看)或Json::FastWriter
对象(常用于数据传输),两者唯一的区别就是序列化字符串的格式不同。 - 调用
writer.write(value)
成员函数返回Json::Value
对象的序列化结果(类型string)
反序列化
- 定义
Json::Value
对象 - 定义
Json::Reader
对象 - 调用
reader.parse(str,value)
函数反序列化,将键值对填充到Json::Value
对象中 - 取用键值对,使用
value["x"].asInt()
的方法按类型将value取出。
#pragma once
#include <sstream>
#include <jsoncpp/json/json.h>#define SEP "|"
#define SPACE " "struct Request
{int _l;int _r;char _op;Request() {}Request(int l, int r, char op): _l(l), _r(r), _op(op){}//序列化std::string Serialize(){//定义万能对象Json::ValueJson::Value root; //填充对象 key-valueroot["l"] = _l;root["r"] = _r;root["op"] = _op;//定义序列化对象Json::FastWriter或StyledWriterJson::FastWriter writer;//调用Json::FastWriter::write将Value对象序列化return writer.write(root);}//反序列化bool Deserialize(const std::string &str){//定义万能对象Json::ValueJson::Value root;//定义反序列化对象Json::ReaderJson::Reader reader;//调用Json::Reader::parse将字节流反序列化reader.parse(str, root);//按类型取出Value对象中的数据_l = root["l"].asInt();_r = root["r"].asInt();_op = root["op"].asInt();return true;}
};
3.3 服务端代码
tcp_server.hpp
#pragma once#include <string>
#include <vector>
#include <pthread.h>
#include <cstdio>
#include <functional>
#include "mysocket.hpp"
#include "protocol.hpp"
#include "logmessage.hpp"class TcpServer
{using func_t = std::function<void(int)>;std::string _ip;uint16_t _port;Socket _listensock;std::vector<func_t> _funcs; //任务队列,可以提供多项服务struct ThreadData{int _sock;TcpServer *_self;ThreadData(int sock, TcpServer *self): _sock(sock), _self(self){}};public:TcpServer(const std::string &ip, uint16_t port): _ip(ip), _port(port){_listensock.Bind(_ip, _port);_listensock.Listen();}void AddService(func_t func){_funcs.push_back(func);}void Excute(int sock){for (auto f : _funcs){f(sock);}}void Start(){while (true){// 1.获取新链接std::string client_ip;uint16_t client_port;int svcsock = _listensock.Accept(&client_ip, &client_port);if (svcsock == -1)continue;LogMessage(NORMAL, "[%s:%d] join, accept a new link!", client_ip.c_str(), client_port);// 2.创建新线程为连接提供服务pthread_t tid;ThreadData *ptd = new ThreadData(svcsock, this);pthread_create(&tid, nullptr, ThreadRoutine, ptd);}}static void *ThreadRoutine(void *args){pthread_detach(pthread_self());// ThreadData *td = (ThreadData *)args;ThreadData *td = static_cast<ThreadData *>(args);td->_self->Excute(td->_sock); //执行任务队列中的任务LogMessage(NORMAL, "client quit!");close(td->_sock);delete td;}
};
tcp_server.cc
#include <memory>
#include <signal.h>
#include <iostream>
#include "tcp_server.hpp"
#include <unistd.h>void Usage(const char *proc)
{printf("\nUsage: %s port\n", proc);
}Response calchelper(const Request &req)
{Response resp(0, 0);switch (req._op){case '+':resp._ret = req._l + req._r;break;case '-':resp._ret = req._l - req._r;break;case '*':resp._ret = req._l * req._r;break;case '/':if (req._r == 0)resp._code = 1;elseresp._ret = req._l / req._r;break;case '%':if (req._r == 0)resp._code = 2;elseresp._ret = req._l % req._r;break;default:resp._code = 3;break;}return resp;
}void *calculator(int sock)
{std::string inbuffer; //应用层缓冲区while (true){// 1.读取成功bool ret = Receive(sock, &inbuffer);if (!ret)break;// std::cout << inbuffer << std::endl;// 2.协议解析,得到一个完整的报文std::string package = Decode(inbuffer);if (package.empty()) //报文不完成,继续读取continue;// std::cout << package << std::endl;// std::cout << inbuffer << std::endl;LogMessage(NORMAL, "%s", package.c_str());// 3.反序列化,字节流->结构化数据Request req;req.Deserialize(package);// 4.业务逻辑Response resp = calchelper(req);// 5.序列化业务的处理结果std::string respstr = resp.Serialize();// std::cout << respstr << std::endl;// 6.添加报头(长度信息),形成一个完成的报文respstr = Encode(respstr);// std::cout << respstr << std::endl;Send(sock, respstr);}
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}uint16_t port = atoi(argv[1]);// server在编写时要有较为严谨的判断逻辑// 一般服务器是要忽略SIGPIPE信号的,防止在运行过程中出现非法写入的问题std::unique_ptr<TcpServer> psvr(new TcpServer("0.0.0.0", port));psvr->AddService(calculator);daemon(0,0); //守护进程化psvr->Start();return 0;
}
3.4 客户端代码
tcp_client.hpp
#pragma once#include <iostream>
#include <string>
#include "mysocket.hpp"
#include "protocol.hpp"
#include "logmessage.hpp"class TcpClient
{Socket _sockfd;std::string _svrip;uint16_t _svrport;public:TcpClient(const std::string &ip, uint16_t port): _svrip(ip), _svrport(port){_sockfd.Connect(ip, port);}void Run(){std::string inbuffer; //应用层缓冲区bool quit = false;while (!quit){// 1.获取需求Request req;std::cin >> req._l >> req._op >> req._r;// 2.序列化std::string reqstr = req.Serialize();std::string temp = reqstr;// 3.添加长度报头reqstr = Encode(reqstr);// 4.向服务端发送数据Send(_sockfd.getsock(), reqstr);while (true){// 5.从服务端接收数据bool ret = Receive(_sockfd.getsock(), &inbuffer);if (!ret){quit = true;break;}// 6.获取一个完成的报文std::string respstr = Decode(inbuffer);if (respstr.empty()) //报文不完整,继续接收continue;// 7.反序列化Response resp;resp.Deserialize(respstr);// 8.输出结果// std::cout << "ret: " << resp._ret << std::endl;// std::cout << "code: " << resp._code << std::endl;std::string err;switch (resp._code){case 1:err = "除0错误";break;case 2:err = "模0错误";break;case 3:err = "非法操作";break;default:std::cout << temp << " = " << resp._ret << " [success]" << std::endl;break;}if (!err.empty())std::cerr << err << std::endl;break;}}}
};
tcp_client.cc
#include "tcp_client.hpp"
#include <memory>
#include <string>void Usage(const char *proc)
{printf("\nUsage: %s svrip svrport\n", proc);
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string svrip = argv[1];uint16_t svrport = atoi(argv[2]);std::unique_ptr<TcpClient> pcli(new TcpClient(svrip, svrport));pcli->Run();return 0;
}
四、重谈协议分层
我们设计的网络服务器自然而然就完成了OSI七层模型中的上三层功能:
- 应用层:解释结构体中的每个字段,并完成了网络计算器的服务
- 表示层:定义固有的结构化数据,并通过序列化和反序列化完成了数据格式的转换,通过添加报头保证了报文的单独性和完整性。
- 会话层:服务器接收到新链接后通过创建新线程为每个链接提供服务,同时新线程也负责管理链接的断开
在TCP/IP五层模型中,将OSI模型中的上三层归为一层应用层。这是因为对于不同的网络服务上三层所对应的具体工作可能不同,操作系统不能统一设计一种协议适用于所有服务,因此将这三层归为一层交给用户自行实现或选择。