目录
🐼接口介绍
✅socket
和Udp类似

socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符;应用程序可以像读写文件一样用 read/write 在网络上收发数据;如果 socket()调用出错则返回-1;对于 IPv4, family 参数指定为 AF_INET;对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议 protocol 参数的介绍从略,指定为 0 即可
✅bind
和Udp类似

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号;
bind()成功返回 0,失败返回-1。bind()的作用是将参数 sockfd 和 myaddr 绑定在一起, 使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号;前面讲过,struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度;
服务器bind struct sockaddr_in是这样的:
网络地址为 INADDR_ANY, 这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址, 这样设置可以在所有的 IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址
✅listen

TCP是面向连接的,服务器一般都是一个比较被动的状态,要等待客户端和他建立连接关系。所以他需要不断保持一个监听的状态 listen
listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略,最佳建议 :直接设置为一个较大的数字(如 128)即可。对于绝大多数常规应用场景,这已经完全足够,你通常不需要为此过多烦恼。 listen()成功返回 0,失败返回-1
通过socket->bind->listen我们就初始化好了一个Tcp有连接的,能够监听的服务器
初始化一个Tcp服务器代码:
cpp
void Init()
{
// 1.socket --- create a sockfd
_listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listenfd < 0)
{
LOG(LogLevel::FATAL) << "create a sockfd err";
exit(SOCKET_CREATE_ERR);
}
LOG(LogLevel::INFO) << "create a sockfd success, sockfd: " << _listenfd;
// 2.bind --- bind a name to a socket(ip+port)
InetAddr local(_port);
int n = bind(_listenfd, (const struct sockaddr *)local.Addr(), local.len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind a name to a socket(ip+port) err";
exit(SOCKET_BIND_ERR);
}
LOG(LogLevel::INFO) << "bind a name to a socket(ip+port) success";
// 3.listen --- listen for connections on a socket
int m = listen(_listenfd, gbacklog);
if (m < 0)
{
LOG(LogLevel::FATAL) << "listen for connections on a socket err";
exit(SOCKET_LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen for connections on a socket success";
}
✅accept
当服务器运行起来,需要通过accept来获取上来listen监听的连接,也就是客户端的连接请求。
三次握手完成后, 服务器调用accept()接受连接;
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
有新连接,accept返回,就获取新连接
addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
如果给addr 参数传NULL,表示不关心客户端的地址;addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
accept的返回值是什么???

如果获取新链接成功,就会返回一个新的文件描述符。什么,返回一个新的文件描述符。socket不是已经返回文件描述符了吗,为什么accept还要返回文件描述符?
答案Tcp会产生更多的文件描述符。讲个故事:
假想我们在假期去一个热门景点游玩,中午时很多餐馆都会派人在门口招揽客人。比如你们走到一家鱼庄门口,店员张三立即迎上来,热情介绍特色菜品、邀请你们用餐。你们正好饿了,就跟着他走进餐厅。但进门之后,张三并没有留下来服务,而是叫来服务员李四为你们点菜倒水,自己则又转身出去招揽下一批客人。之后,张三不断引来新客人,而店内也有李四、王五、赵六等服务员依次接手服务------张三只负责拉客,服务员只专注服务,各尽其责,配合默契,整个鱼庄生意井然有序、越来越旺。
对应到网络编程,listensock 就像是张三,专门负责与客户端建立连接;而 accept 返回的 sockfd 就像是李四,专门为已连接的客户端提供具体服务。
♐细节一:accept获取上来的新连接在Tcp三次握手之后就已经完成了。
♐细节二:在 UDP 中,我们通过recvfrom来获取发送方(客户端)的地址信息。而在 TCP 中,则是通过accept来接受一个新的连接,并同时获得代表该连接的套接字以及对端客户端的信息。这也从机制上体现出 TCP 是面向连接的协议:每次连接都需要经过明确的建立过程,并且通常会保持一段时间的通信状态,因此需要一个专有的服务流程来管理和维护这些连接。
服务器Run获取新连接代码:
cpp
void Run()
{
while(true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须初始化
int sockfd = accept(_listenfd, (struct sockaddr*)&peer, &len);
InetAddr client(peer);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept: " << client.SocketToString() << " err";
continue; // 跳过本次IO
}
LOG(LogLevel::INFO) << "accept a new connection: " << client.SocketToString() << " success";
HandlerIO(sockfd, client);
}
}
我们可以使用telnet 工具来测试我们的服务器是否起来,telnet ip+port:

这里也验证了,客户端请求端口号os在内核中会随机分配。
下面我们就可以对新获取上来的连接进行IO或者其他操作了。取决于你的服务器是什么.
✅客户端接口connect

connect函数的作用是由一个套接字(通常称为客户端)主动向另一个套接字(服务器)发起连接请求 。它的核心作用是在客户端应用程序中,将本地创建的套接字与一个指定的远程服务器地址进行绑定和连接初始化。
connect 会触发 TCP 三次握手过程。客户端内核会向指定的服务器地址发送 SYN 包,开始建立连接。
如果连接成功建立(服务器接受了请求并完成了握手),connect 调用才返回成功 (0)。此时,这个套接字就正式与服务器端建立了一条可靠的、双向的通信通道,之后就可以使用 send 和 recv 来可靠地传输数据。我们等下会验证这条双向连接!而UDP不需要connect,本质不需要三次握手,是无连接的。
connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址(ip+port), 而connect 的参数是对方的地址,直到自已要连接的服务器是谁
🐼EchoServer
下面我们先将服务器打造成EchoServer
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Logger.hpp"
#include "Comm.cc"
#include "InetAddr.hpp"
const static int deflistenfd = -1;
static const int gbacklog = 128;
class EchoServer
{
public:
EchoServer(uint16_t port) : _listenfd(deflistenfd), _port(port)
{
}
void Init()
{
// 1.socket --- create a sockfd
_listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listenfd < 0)
{
LOG(LogLevel::FATAL) << "create a sockfd err";
exit(SOCKET_CREATE_ERR);
}
LOG(LogLevel::INFO) << "create a sockfd success, sockfd: " << _listenfd;
// 2.bind --- bind a name to a socket(ip+port)
InetAddr local(_port);
int n = bind(_listenfd, (const struct sockaddr *)local.Addr(), local.len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind a name to a socket(ip+port) err";
exit(SOCKET_BIND_ERR);
}
LOG(LogLevel::INFO) << "bind a name to a socket(ip+port) success";
// 3.listen --- listen for connections on a socket
int m = listen(_listenfd, gbacklog);
if (m < 0)
{
LOG(LogLevel::FATAL) << "listen for connections on a socket err";
exit(SOCKET_LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen for connections on a socket success";
}
void Run()
{
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须初始化
int sockfd = accept(_listenfd, (struct sockaddr *)&peer, &len);
InetAddr client(peer);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept: " << client.SocketToString() << " err";
continue; // 跳过本次IO
}
LOG(LogLevel::INFO) << "accept a new connection: " << client.SocketToString() << " success";
HandlerIO(sockfd, client);
}
}
~EchoServer()
{
if (_listenfd > 0)
{
close(_listenfd);
}
}
private:
void HandlerIO(int sockfd, InetAddr &client)
{
while (true)
{
char buff[1024];
ssize_t n = read(sockfd, buff, sizeof(buff) - 1);
if (n < 0)
{
LOG(LogLevel::ERROR) << "read from: " << sockfd << " err";
continue;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "peer exit, me too!";
break;
}
else
{
buff[n] = 0;
std::string echo_string = "Server say@";
echo_string += buff;
int m = write(sockfd, echo_string.c_str(), echo_string.size());
(void)m;
}
}
close(sockfd); // 一定要关闭,防止文件描述符泄露
}
private:
int _listenfd;
uint16_t _port;
};
客户端的代码:
cpp
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
void Usage(const std::string &msg)
{
printf("Usage: %s + ip + port\n", msg.c_str());
}
int main(int argc, char *argv[])
{
// 1. 启动日志
EnableConsoleLogStrategy();
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd <0 )
{
LOG(LogLevel::DEBUG) << "create a socket err";
}
InetAddr server(port, ip); // 已知对端服务器ip+port
while(true)
{
int ret = connect(sockfd, (const sockaddr*)server.Addr(), server.len());
if( ret == 0)
break;
else
LOG(LogLevel::WARNING) << "connect err";
}
char buff[1024];
while(true)
{
std::cout << "Please Enter#";
std::string line;
std::getline(std::cin, line);
ssize_t n = write(sockfd, line.c_str(), line.size());
if(n > 0)
{
buff[0] = 0; // O(1)清空
int m = read(sockfd, buff, sizeof(buff)-1);
if(m >0 )
{
buff[m] = 0;
std::cout << buff << std::endl;
}
}
}
return 0;
}
✅细节一:这里注意一下。服务器accept默认是不知道是哪个客户端发起向自已的请求的。而客户端默认是知道自已向哪个客户端请求的。注意一下accept的参数和connect的参数是输入还是输出型参数。
✅细节二:我们使用netstat来查看一下我们启动的服务器和客户端状态

由于我们的客户端和服务器是在同一台主机上,导致客户端和服务器的进程查看都能查到。我们发现既有服务器到客户端的连接,也有客户端到服务器的连接。这条连接是双向的,全双工的。当客户端在不同主机上,我们就只能查到服务器到客户端的这一条连接了。
上面这段服务器代码有一个问题。就是当服务器进入HandlerIO时,在read阻塞了。或者这一条连接对端不退出,这就会导致我们的服务器获取不到其他客户端在同一时间的请求,必须等到这个服务结束了才会去进行下一个客户端的连接,这显然是不合理的。为了改进,我们将我们的服务器改造成多执行流的。
🏄多进程EchoServer
思路:为了不使我们的主进程阻塞。这里有两种方案,一是创建子进程处理HanderIO,然后signal忽略SIGCHLD信号(最佳实践)。这样,父进程就不用waitpid等待,让系统傍明回收,就不会阻塞。
二是子进程再创建孙子进程,然后立马退出,父进程waitpid没有阻塞。孙子进程会被系统领养 由系统回收,每一个孙子进程执行完HandlerIO后退出即可。我们这里采用这种
关键改动代码:
cpp
void Run()
{
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须初始化
int sockfd = accept(_listenfd, (struct sockaddr *)&peer, &len);
InetAddr client(peer);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept: " << client.SocketToString() << " err";
continue; // 跳过本次IO
}
LOG(LogLevel::INFO) << "accept a new connection: " << client.SocketToString() << " success";
// version1---多进程版本
pid_t id = fork();
if(id < 0)
{
LOG(LogLevel::WARNING) << "资源不足,无法创建子进程";
exit(FORK_ERR);
}else if(id == 0)
{
// 孙子进程去执行,子进程退出
if(fork()>0)
{
exit(OK);
}
// 孙子进程,交给系统1领养,主进程无需关心了
close(_listenfd); // 孙子进程关闭listenfd,无需关心,防止误操作
HandlerIO(sockfd, client);
// close(sockfd); // HandlerIO中已经关闭了
exit(OK); // 处理完,孙子进程一定要退出
}
pid_t rid = waitpid(id, nullptr, 0); // 子进程创建完孙子进程立马退出
(void)rid;
close(sockfd); // 父进程关闭sockfd,无需关心,防止误操作
}
}
当有三条连接结果:

下面有几个细节
✅细节一:关掉不必要的文件描述符。由于孙子进程也能看到其父进程的文件描述符,为了防止误操作,将父进程的listenfd关闭,同理;父进程也要关闭其孙子进程的sockfd,因为父进程不需要HandlerIO,正是因为他们否不需要关心,每个fd都有自已应该有的使命,各找个的妈,所以才需要关闭自已不需要的fd。孙子进程只需要关心自已的HandlerIO,只关心sockfd,然后执行完退出。父进程只关心listenfd, 需要监听获取sockfd即可。
✅细节二:由于父进程关闭了sockfd,这就会使每一个新的孙子进程获取到的sockfd都是从4开始。我们可以验证:

🏄多线程EchoServer
由于多进程太耗费资源了!!所以我们应该考虑多线程版!!如果join的话又会阻塞住,所以我们可以直接将线程给分离了,这样主线程就不关心了!
关键代码:
cpp
// 创建一个内部类
struct ThreadData
{
InetAddr _addr;
int _sockfd;
EchoServer *_esvr;
ThreadData(InetAddr& addr, int sockfd, EchoServer *esvr)
: _addr(addr), _sockfd(sockfd), _esvr(esvr)
{
}
~ThreadData() {}
};
void Run()
{
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须初始化
int sockfd = accept(_listenfd, (struct sockaddr *)&peer, &len);
InetAddr client(peer);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept: " << client.SocketToString() << " err";
continue; // 跳过本次IO
}
LOG(LogLevel::INFO) << "accept a new connection: " << client.SocketToString() << " success";
pthread_t tid;
ThreadData *data = new ThreadData(client, sockfd, this); // 内部类
pthread_create(&tid, nullptr, Routine, (void *)data);
pthread_detach(tid); // 线程分离,无序等待
}
}
static void *Routine(void *args)
{
ThreadData* data = static_cast<ThreadData*>(args);
EchoServer *self = data->_esvr;
self->HandlerIO(data->_sockfd, data->_addr);
// 归还资源
delete data;
pthread_exit(0); // 线程运行完自已退出
return (void*)nullptr;
}
void HandlerIO(int sockfd, InetAddr &client)
{
... 处理输入输出
close(sockfd); // 必须关,防止文件描述符泄露
}
✅细节一:由于文件描述符表是共享的,当服务器每收到一个客户端请求时,fd就会+1,是递增的。主线程不需要关闭,等待新线程执行完再close即可。
✅细节二:新线程归还资源的时机是很重要的,要在HandlerIO后归还资源。
✅细节三:为了在类内部使用pthread_create的回到函数,Routine需要为静态成员函数,且我们需要调用HandlerIO,所以→创建了个内部类方便管理。因为void*可以强转为任意数据类型
🏄线程池EchoServer
无论是多进程还是多线程,都会有两个问题。
1 效率问题,创建进程线程(接收到socket才开始创建)
2. 执行流个数没有上限。这可能会导致未知错误。比如当一个黑客恶意攻击我们的服务器,导致我们的服务器资源过载挂掉。所以我们需要给我们的服务器设置上限,当请求大于我们的上限,不受理。
关键代码:
cpp
void Run(ThreadPool<Task_t>* t)
{
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须初始化
int sockfd = accept(_listenfd, (struct sockaddr *)&peer, &len);
InetAddr client(peer);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept: " << client.SocketToString() << " err";
continue; // 跳过本次IO
}
LOG(LogLevel::INFO) << "accept a new connection: " << client.SocketToString() << " success";
// version3 --- 线程池版本
// Task_t task = std::bind(&EchoServer::HandlerIO, this, sockfd, client);
Task_t task = [this, sockfd, client](){
this->HandlerIO(sockfd, client);
};
t->Enqueue(task);
}
void HandlerIO(int sockfd, InetAddr client)
{
while (true)
{
char buff[1024];
ssize_t n = read(sockfd, buff, sizeof(buff) - 1);
if (n < 0)
{
LOG(LogLevel::ERROR) << "read from: " << sockfd << " err";
continue;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "peer exit, me too!";
break;
}
else
{
buff[n] = 0;
std::string echo_string = "Server say@";
echo_string += buff;
int m = write(sockfd, echo_string.c_str(), echo_string.size());
(void)m;
}
}
close(sockfd); //必须关,防止文件描述符泄露
}
// main函数调用
void Usage(const std::string &msg)
{
printf("Usage: %s + port\n", msg.c_str());
}
int main(int argc, char *argv[])
{
// 1. 启动日志
EnableConsoleLogStrategy();
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
// 2. 启动线程池
ThreadPool<Task_t>* t = ThreadPool<Task_t>::GetInstance();
t->Start();
std::unique_ptr<EchoServer> esvr = std::make_unique<EchoServer>(port) ;
esvr->Init();
esvr->Run(t);
return 0;
}
✅细节一:我们上述的服务器不适合用线程池来做。线程池满足的都是短连接,迅速的任务。而如果是长连接,可能导致同一时间如果多个客户端请求,由于是长连接,每个线程都有"活", 服务器被打满的问题,这就会导致客户端连接丢失。
✅细节二:通过bind绑定成员函数和对应的参数,返回值是function类型的接受即可。也可用lambda,注意传值还是传引用。
🐼CommandServer
如果我们想把我们的服务器从EchoServer来改成CommandServer,也就是可以处理用户请求的命令,处理完后返回给用户,就像xshell ssh一样。下面我们来模拟这个过程。
首先,需要清楚一点,解析command执行cmd这个过程不属于服务器管,应该交给上层,我们需要完成解耦工作。
下面我们还是使用多线程来完成多执行流工作。
主要代码改动:
cpp
// 服务器
using callback_t = std::function<std::string(const std::string& )>;
void HandlerIO(int sockfd, InetAddr client)
{
while (true)
{
char buff[1024];
ssize_t n = read(sockfd, buff, sizeof(buff) - 1);
if (n < 0)
{
LOG(LogLevel::ERROR) << "read from: " << sockfd << " err";
continue;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "peer exit, me too!";
break;
}
else
{
// 更换这里的逻辑
buff[n] = 0;
std::string result = _cb(buff); // 回调到上层,访问上层业务
int m = write(sockfd, result.c_str(), result.size());
(void)m;
}
}
close(sockfd); // 必须关,防止文件描述符泄露
}
// Command.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
class Command
{
private:
bool IsExists(const std::string &cmd)
{
for (auto &e : _command_white_list)
{
if (cmd == e)
return true;
}
return false;
}
void ShowLists()
{
for (auto &e : _command_white_list)
{
std::cout << e << std::endl;
}
}
public:
Command()
{
_command_white_list.push_back("ls -a -l");
_command_white_list.push_back("cat test.txt");
_command_white_list.push_back("tree");
_command_white_list.push_back("whoami");
_command_white_list.push_back("who");
_command_white_list.push_back("pwd");
}
std::string Exec(const std::string &cmd)
{
if (!IsExists(cmd))
{
return "Illegal: " + cmd;
}
FILE *fp = popen(cmd.c_str(), "r");
if (fp == nullptr)
{
return "popen err: " + cmd;
}
else
{
std::cout << cmd << std::endl;
std::string result;
char buff[1024];
while (fgets(buff, sizeof buff, fp) != nullptr)
{
result += buff;
}
pclose(fp);
std::cout << cmd << std::endl;
return result;
}
}
~Command() {}
private:
std::vector<std::string> _command_white_list; // 增加白名单,防止用户恶意访问
};
// 主函数调用修改,绑定回调函数
int main(int argc, char *argv[])
{
// 1. 启动日志
EnableConsoleLogStrategy();
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<Command> cmd = std::make_unique<Command>();
std::unique_ptr<CmdServer> esvr = std::make_unique<CmdServer>(port, [&cmd]
(const std::string& in)->std::string{
return cmd->Exec(in);
}) ;
esvr->Init();
esvr->Run();
return 0;
}
🐼地址转换函数
我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示 和in_addr(4字节)表示之间转换;
字符串转 in_addr 的函数(主机转网络):

in_addr 转字符串的函数(网络转主机):

在我们的网络地址转换函数中,使用的是inet_aton和inet_ntoa。但是这里有一个问题,这里以inet_ntoa为例:inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果. 那么是否需要调用者手动释放呢?man 手册上说, inet_ntoa 函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
如果我们调用多次这个函数, 会有什么样的效果呢


因为 inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。这就表明inet_ntoa并不是线程安全的函数,当多个执行流同时访问这个静态存储区,会受影响。如何做呢?给每个地址都有属于自已的静态存储区。
最佳实践,以后地址转换,都使用Inet_ntop和inet_pton;
cpp
void Host2Net()
{
memset(&_addr, 0, sizeof _addr);
_addr.sin_family = AF_INET; // 协议族
_addr.sin_port = htons(_port);
// _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
inet_pton(AF_INET, _ip.c_str(), &(_addr.sin_addr.s_addr));
}
void Net2Host()
{
_port = ntohs(_addr.sin_port);
// _ip = inet_ntoa(_addr.sin_addr);
char ipbuff[64];
inet_ntop(AF_INET, &(_addr.sin_addr.s_addr), ipbuff, sizeof(ipbuff));
_ip = ipbuff;
}
🐼read和write
不管是客户端还是服务器,在接受和发送数据时,我们都使用了read和write接口。但是这里有一个问题:write是直接将数据写给网络?read直接从网络中读?从系统角度我们就知道,一定不是,read和write是系统的接口,并没有能力直接将数据交给网络。那交给谁呢?
在我们创建一个socket,os系统会给每一个sockfd维护一个接受缓冲区和发送缓冲区,而write本质是将数据写到发送缓冲区,read本质是从接受缓冲区中读,所以write和read本质上是拷贝函数!就和printf一样。只是将数据拷贝给发送缓冲区或者将接受缓冲区的数据进行拷贝,没有能力贯穿网络协议栈。每一个sockfd都是如此。
我们后面用send/recv本质上也是拷贝函数。
✅细节一: 既然双方的sockfd都维护了两个缓冲区,这也是不是意味着双方既可以发送,又可以接受,这才是为什么Tcp支持全双工的原因。
✅细节二: 假设主机A的发送缓冲区已经有数据"ls -l -a",什么时候将这个数据发送给对端的接受缓冲区,完全由主机A的Tcp自主决定,这也就是为什么Tcp叫做传输控制协议。
✅细节三: 而如果主机A并没有将数据发给对端的接受缓冲区,对端就会阻塞,这像不像生产者消费者模型。主机A即是消费者又是生产者,主机B同理。
✅细节四:我们发送的数据报是否完整,是否是一个完整的报文发送到主机B的接受缓冲区,Tcp并不关心,这完全由我们用户自主控制。如果我们用户没有自主控制,数据报粘包问题可能就会导致意外错误
将报文从主机A发送给主机B接受发送缓冲区过程大致如图:

🐼代码存在的问题
我们已经知道了UdpServer和TcpServer的服务器编写,Udp对无listen状态,并且对心连接只读一次,但是TcpServer不仅要listen,还要一直维持这一条连接,直到客户端退出,自已才断掉这条连接,这是由read的返回值决定的。
但是Tcp服务器这里有一个问题,就是在IO处理时,可能并没有把一条完整报文读上来!为什么没有读上来?因为Tcp服务器是面向字节流的,我也不知道什么时候读上来一条完整报文,然后我就交给上层处理了,这显然是不对的。在建立网络共识时,曾经说过,既要明白如何切割出一个有效载荷,要有能力将报文和有效载荷分开。
但是我们这里并没有做到?如何做,那就交给双方自定义的协议吧~