铁佛 发表于 2024-6-20 20:27:41

简单的TCP网络步伐:英译汉服务器

一、服务器的初始化

下面先容步伐中用到的socket API,这些函数都在sys/socket.h中。
1.创建套接字

socket():
https://img-blog.csdnimg.cn/direct/455b5c53b2e94bf986d156eeb2f60ac0.png
   ⭐参数先容:


[*]socket()打开一个网络通讯端口,如果乐成的话,就像open()一样返回一个文件描述符;
[*]应用步伐可以像读写文件一样用read/write在网络上收发数据;
[*]如果socket()调用堕落则返回-1;
[*]对于IPv4, family参数指定为AF_INET;
[*]对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
[*]protocol参数的先容从略,指定为0即可。
class TcpServer
{
public:
    TcpServer()
      : _sockfd(defaultsockfd)
    {
    }
    void Init()
    {
      _sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (_sockfd < 0) // 创建套接字失败
      {
            lg(Fatal, "create socket, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
      }
      lg(Info, "create socket success, socket: %d", _sockfd);
    }
    ~TcpServer()
    {
      close(_sockfd);
    }

private:
    int _sockfd; // 套接字
}; 2.绑定端口号和ip

bind():
https://img-blog.csdnimg.cn/direct/39aace9c925c437b87c9fb29796e55ec.png
   ⭐参数先容:


[*]服务器步伐所监听的网络地点和端口号通常是固定不变的,客户端步伐得知服务器步伐的地点和端口号后 就可以向服务器发起毗连; 服务器必要调用bind绑定一个固定的网络地点和端口号;
[*]bind()乐成返回0,失败返回-1。
[*]bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 myaddr所描述的地点和端口号;
[*]前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数现实上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以必要第三个参数addrlen指定布局体的长度;
我们先来看看地点转换函数
本节只先容基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地点 但是我们通常用点分十进制的字符串表示IP 地点,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
https://img-blog.csdnimg.cn/direct/fd5df61e53e24e12bd675700a442b52b.png
in_addr转字符串的函数:
https://img-blog.csdnimg.cn/direct/9ea9c2832ab04731b7c2d9c0261ce1b5.png
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是 否必要调用者手动释放呢?
https://img-blog.csdnimg.cn/direct/d012fce16b8d42939d8c302e25c6c165.png
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不必要我们手动举行释放. 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
https://img-blog.csdnimg.cn/direct/0fa272aa455047ff9ee4bb1d7ea3744c.png
因为inet_ntoa把结果放到自己内部的一个静态存储区, 如许第二次调用时的结果会覆盖掉上一次的结果


[*]思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
[*]在APUE中, 明确提出inet_ntoa不是线程安全的函数;
[*]但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
[*]自己写步伐验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
[*]在多线程情况下, 保举利用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问 题;
我们的步伐中对myaddr参数是如许初始化的:
https://img-blog.csdnimg.cn/direct/59fbb2a9bfb243a0af3e3abe7231a29d.png
   

[*]1. 将整个布局体清零;
[*]2. 设置地点类型为AF_INET;
[*]3. 网络地点为0.0.0.0, 这个宏表示本地的恣意IP地点,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地点, 如许设置可以在所有的IP地点上监听,直到与某个客户端建立了毗连时才确定下来到底用 哪个IP 地点;
class TcpServer
{
public:
    TcpServer(const uint16_t& port, const string& ip = defaultip)
      : _sockfd(defaultsockfd)
      , _port(port)
      , _ip(ip)
    {
    }
    void Init()
    {
      // 1.创建套接字
      _sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (_sockfd < 0) // 创建套接字失败
      {
            lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
      }
      lg(Info, "create socket success, socket: %d", _sockfd);

      //2.绑定端口号
      // 使用这个结构体需要包头文件
      struct sockaddr_in local;
      memset(&local, 0, sizeof(local));
      local.sin_family = AF_INET;
      // 转化为网络序列
      local.sin_port = htons(_port);
      // 字符串转化为点时分形式的ip
      inet_aton(_ip.c_str(), &(local.sin_addr));
      //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
      int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
      if(n < 0)
      {
            lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
      }
    }
    ~TcpServer()
    {
      close(_sockfd);
    }

private:
    int _sockfd; // 套接字
    uint16_t _port; // 端口号
    string _ip; // ip地址
}; 3.设置监听状态

listen():
https://img-blog.csdnimg.cn/direct/4d9b3fb8aee9479fa8ba3b7a8abb3545.png
   

[*]listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于毗连等待状态, 如果接收到更多 的毗连请求就忽略, 这里设置不会太大(一样平常是5);
[*]listen()乐成返回0,失败返回-1;
#pragma onec

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
using namespace std;

const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
Log lg;

enum
{
    UsageError = 1,
    SocketError = 2,
    BindError = 3,
    ListenError = 4
};

class TcpServer
{
public:
    TcpServer(const uint16_t& port, const string& ip = defaultip)
      : _sockfd(defaultsockfd)
      , _port(port)
      , _ip(ip)
    {
    }
    void Init()
    {
      // 1.创建套接字
      _sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (_sockfd < 0) // 创建套接字失败
      {
            lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
      }
      lg(Info, "create socket success, socket: %d", _sockfd);

      //2.绑定端口号
      // 使用这个结构体需要包头文件
      struct sockaddr_in local;
      memset(&local, 0, sizeof(local));
      local.sin_family = AF_INET;
      // 转化为网络序列
      local.sin_port = htons(_port);
      // 字符串转化为点时分形式的ip
      inet_aton(_ip.c_str(), &(local.sin_addr));
      //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
      int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
      if(n < 0)
      {
            lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
      }
      lg(Info, "bind socket success, socket: %d", _sockfd);


      // 3.设置监听状态
      // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
      if (listen(_sockfd, backlog) < 0)
      {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
      }   
      lg(Info, "bind socket success, socket: %d", _sockfd);

      
    }
    ~TcpServer()
    {
      close(_sockfd);
    }

private:
    int _sockfd; // 套接字
    uint16_t _port; // 端口号
    string _ip; // ip地址
}; 4.设置服务器的端口号和ip

未来我们向让用户设置端口号,所以我们可以在main函数中借助命令行参数通报,对于ip我们就直接利用默认的ip参数"0.0.0.0"即可。
#include "TcpServer.hpp"

#include <memory>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port\n" << std::endl;
}

// ./tcpserver 8080
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
      Usage(argv);
      exit(UsageError);
    }
    uint16_t port = std::stoi(argv);
    unique_ptr<TcpServer> tcpSvr(new TcpServer(port));
    tcpSvr->Init();
    //tcpSvr->Start();
    return 0;
} 现在我们就可以来测试一下啦!
https://img-blog.csdnimg.cn/direct/54b0756c7451496e9d1ee95637f164c5.png
二、服务器的运行

1.建立新链接

accept():
https://img-blog.csdnimg.cn/direct/f349fb021bda4a0fb5fede583dd16f3c.png
   

[*]三次握手完成后, 服务器调用accept()接受毗连;
[*]如果服务器调用accept()时还没有客户端的毗连请求,就阻塞等待直到有客户端毗连上来;
[*]addr是一个传出参数,accept()返回时传出客户端的地点和端口号;
[*]如果给addr参数传NULL,表示不关心客户端的地点;
[*]addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地点布局体的现实长度(有可能没有占满调用者提供的缓冲区);
[*]获取毗连乐成返回接收到的套接字的文件描述符,获取毗连失败返回-1,同时错误码会被设置。
⭐accept函数返回的套接字是什么?
调用accept函数获取毗连时,是从监听套接字当中获取的。如果accept函数获取毗连乐成,此时会返回接收到的套接字对应的文件描述符。
⭐监听套接字与accept函数返回的套接字的作用:
   

[*]监听套接字:用于获取客户端发来的毗连请求。accept函数会不断从监听套接字当中获取新毗连。
[*]accept函数返回的套接字:用于为本次accept获取到的毗连提供服务。监听套接字的任务只是不断获取新毗连,类似于餐厅门口的迎宾,而真正为这些毗连提供服务的套接字是accept函数返回的套接字,类似于餐厅的服务员,而不是监听套接字。
所以初始化TCP服务器时创建的套接字应该叫做监听套接字。为了表明寓意,我们将代码中套接字的名字由_sockfd改为_listensocket,如许写着更清楚明了。
#pragma onec

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
using namespace std;

const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
Log lg;

enum
{
    UsageError = 1,
    SocketError = 2,
    BindError = 3,
    ListenError = 4
};

class TcpServer
{
public:
    TcpServer(const uint16_t& port, const string& ip = defaultip)
      : _listensocket(defaultsockfd)
      , _port(port)
      , _ip(ip)
    {
    }
    void Init()
    {
      // 1.创建套接字
      _listensocket = socket(AF_INET, SOCK_STREAM, 0);
      if (_listensocket < 0) // 创建套接字失败
      {
            lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
      }
      lg(Info, "create socket success, socket: %d", _listensocket);

      //2.绑定端口号
      // 使用这个结构体需要包头文件
      struct sockaddr_in local;
      memset(&local, 0, sizeof(local));
      local.sin_family = AF_INET;
      // 转化为网络序列
      local.sin_port = htons(_port);
      // 字符串转化为点时分形式的ip
      inet_aton(_ip.c_str(), &(local.sin_addr));
      //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
      int n = bind(_listensocket, (const struct sockaddr*)&local, sizeof(local));
      if(n < 0)
      {
            lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
      }
      lg(Info, "bind socket success, socket: %d", _listensocket);


      // 3.设置监听状态
      // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
      if (listen(_listensocket, backlog) < 0)
      {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
      }   
      lg(Info, "bind socket success, socket: %d", _listensocket);
    }

    void Start()
    {
      lg(Info, "tcpServer is running....");
      for (;;)
      {
            // 1. 获取新连接 - 知道客户端的ip地址和端口号
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾
            // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员
            int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue; // 所以这里使用continue
            }
            lg(Info, "get a new link..., sockfd: %d", sockfd);
      }
    }

    ~TcpServer()
    {
      close(_listensocket);
    }

private:
    int _listensocket; // 套接字
    uint16_t _port; // 端口号
    string _ip; // ip地址
}; 此时我们想来测试一下,但是我们的客户端还没有写,咋办呢?
telnet 127.0.0.1 8888 是一个网络诊断命令,它的作用是尝试通过Telnet协议毗连到本田主机(本机)的8888端口。这里:


[*]telnet 是命令自己,用于举行长途登录和管理。
[*]127.0.0.1 是IPv4环回地点,指向本地盘算机。利用这个地点,你现实上是尝试毗连到你自己的机器。
[*]8888 是端口号,很多应用步伐和服务会监听特定的端口来接收数据。8888是一个常见的测试或备用端口。
https://img-blog.csdnimg.cn/direct/cd845b9f8abc4ef9acf8e03298bc9635.png
上一个知识点我们提到udp它是不能绑定我们云服务器的公网ip的,但是绑定本地环回127.0.0.1可以的,我们看看tcp可不可以。
https://img-blog.csdnimg.cn/direct/4f58eddc731d49bfbc446b245bfc39b1.png
答案是也不可以绑定我们云服务器的公网ip的。那么绑定本地环回127.0.0.1可以嘛?
https://img-blog.csdnimg.cn/direct/bb064c73b20f4bd2a3ac3ed0cce1ed18.png
所以为了能毗连我们的与服务器的公网ip,我们将服务器的ip设置为0.0.0.0,这就意味着该tcp服务器可以读取服务器任何一个ip,保证一定能毗连上我们的服务器。
2.举行通讯

建立通讯我们起首就要获取到给服务器发送请求的端口号和ip地点
https://img-blog.csdnimg.cn/direct/4cf3fefba7904313af6f688a124e5883.png
我们来测试一下,看看此时能不能接收到客户端的端口号和ip地点。
https://img-blog.csdnimg.cn/direct/ce71cf76559440869c99755ed18bd99e.png
此时我们就能知道我们确实有毗连到我们的服务器了。
⭐注意:我们利用的云服务默认是将端口号禁用了,我们必要在云服务器后台将我们的端口号取消禁用,否则的话我们只能在本地通讯,我们可以利用windows毗连一下服务器,看看出现上面情况。
https://img-blog.csdnimg.cn/direct/eeddebe926ce43b39c2dabb7e523e5f1.png
所以就必要前去服务器举行安全组的设置。
https://img-blog.csdnimg.cn/direct/e9bfc2e2661b4761bdcabb36e7d321c6.png
随后我们再来测试一下哈。
https://img-blog.csdnimg.cn/direct/223086f6e18e47178085acf6c9558dc3.png
同时我们这里还有一个问题,我们之前的端口号和网络序列都要转成网络序列,我们都利用了相应的接口,但是为什么用户发送来的数据,我们不必要手动调用接口转化为网络序列呢?在利用套接字通讯的时候,会默认将用户发送来的数据转化成网络序列,而端口号和网络序列比力特殊,必要写给操作系统的,所以必要我们自己去转的。现在我们就可以举行通讯了,由于tcp是基于数据流的,所以直接利用write和read接口即可。
void Service( int sockfd,const string& ip,const uint16_t& port)
{
    char buffer;
    while(true)
    {
      // 读取用户发送的请求
      ssize_t n = read(sockfd, buffer, sizeof(buffer));
      if(n > 0)
      {
            buffer = '\0';
            cout << "client says: " << buffer << endl;
            string echo_string = "tcpserver echo# ";
            echo_string += buffer;

            write(sockfd, echo_string.c_str(), echo_string.size());
      }
      
    }
} 我们来运行一下:
https://img-blog.csdnimg.cn/direct/deb599279f624bdba052347114fd6d6f.png
此时我们发现就可以通讯啦!!!但是上面的客户端不是我们写的,我们自己来写一个。
三、客户端的初始化

1.创建套接字

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
    cerr << "socket error" << endl;
} 2.绑定端口号和ip

https://img-blog.csdnimg.cn/direct/2d98ee6e3f4d4b0b85d3700f31f13790.png
由于客户端不必要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配
注意:


[*]客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动 多个客户端, 就会出现端口号被占用导致不能正确建立毗连;
[*]服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动 服务器时端口号都不一样, 客户端要毗连服务器就会遇到麻烦;
测试多个毗连的情况
再启动一个客户端, 尝试毗连服务器, 发现第二个客户端, 不能正确的和服务器举行通讯. 分析缘故原由, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接 受新的请求. 我们当前的这个TCP, 只能处置惩罚一个毗连, 这是不科学的.后面我们会改成多线程版本的。
3.毗连服务器

connect:
https://img-blog.csdnimg.cn/direct/c39e39b0e4174f4c93696904be800e12.png
   

[*]客户端必要调用connect()毗连服务器;
[*]connect和bind的参数形式同等, 区别在于bind的参数是自己的地点, 而connect的参数是对方的地点;
[*]connect()乐成返回0,堕落返回-1;
#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>

using namespace std;
void Usage(const string &proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n"
         << endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(1);
    }
    string serverip = argv;
    uint16_t serverport = stoi(argv);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
      cerr << "socket error" << endl;
    }

    // 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
    // tcp客户端要不要bind?一定要显示绑定只不过不需要用户显示的bind!一般有OS自由随机选择!
    // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    // 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
    // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
    // 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
    // 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
    // 系统什么时候给我bind呢?首次发送connect的时候,进行自动随机绑定

    // 可是客户端此时不知道服务器的ip和端口号
    // 使用命令行参数来解决
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
   
    int n = connect(sockfd, (const struct sockaddr*)&server, sizeof(server));
    if(n < 0)
    {
      cerr << "connect error..." << endl;
      exit(2);
    }
    close(sockfd);
    return 0;
} 4.给服务端发送信息

string message;
while (true)
{
    cout << "Please Enter# ";
    getline(cin, message);
    // 发送数据
    int n = write(sockfd, message.c_str(), message.size());
    // 读取数据
    char inbuffer;
    n = read(sockfd, inbuffer, sizeof(inbuffer));
    if (n > 0)
    {
      inbuffer = 0;
      std::cout << inbuffer << std::endl;
    }
} 此时我们的服务器和客户端就可以运行啦!
https://img-blog.csdnimg.cn/direct/566ad48a71fc49529f87d684bc3b88d5.png
四、一些安全问题的处置惩罚

1.客户端直接退出,服务器处置惩罚

客户端直接退出,服务器会怎么样,服务器读取不到客户端发来的请求,此时read返回值为0,应该关闭网络文件描述符,剩余一种情况就是读取失败,我们直接打印警告日志信息。
void Service( int sockfd,const string& ip,const uint16_t& port)
{
    char buffer;
    while(true)
    {
      // 读取用户发送的请求
      ssize_t n = read(sockfd, buffer, sizeof(buffer));
      if(n > 0)
      {
            buffer = '\0';
            cout << "client says: " << buffer << endl;
            string echo_string = "tcpserver echo# ";
            echo_string += buffer;

            write(sockfd, echo_string.c_str(), echo_string.size());
      }
      else if(n == 0)
      {
            // 此时需要关闭文件描述符
            // 我们在返回调用该函数的地方该关闭文件描述符
            lg(Info, "%s:%d quit, server close sockfd: %d", ip.c_str(), port, sockfd);
            break;
      }
      else
      {
            lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));
            break;
      }
    }
} 运行一下:
https://img-blog.csdnimg.cn/direct/bab472b1419948cd8a870977022a6a3c.png
2.单进程服务器,多个客户端毗连

我们直接来看现象
https://img-blog.csdnimg.cn/direct/88fdc3475b6a4903a2b00abf3ac91fa0.png
然后我们让客户端分别写一条数据
https://img-blog.csdnimg.cn/direct/63c49b3ddcd44ae9acef582c33b7e12e.png
我们发现此时客户端1能乐成发送数据并接收数据,但是客户端2不行,随后我们来关闭客户端1.
https://img-blog.csdnimg.cn/direct/407dbc014d5743f1a63a52006c647048.png
此时客户端1一退出,客户端2立马毗连,并且将刚刚的数据发送给服务器并乐成并接收数据。为什么呢?因为此时我们的服务器是一个单进程,一个服务器为一个客户端提供服务时,此时由于我们写的是while循环,此时必须等客户端1退出,才气关闭文件描述符(让),然后这个服务器才会空闲,然后此时客户端2才气建立毗连,服务器才会提供服务。此时就相当于餐厅只有一个服务员,等这个服务员服务好了客户端1,才气接待客户端2,所以我们这里必要改一下,让多个客户端共同利用。
void Start()
{
    lg(Info, "tcpServer is running....");
    for (;;)
    {
      // 1. 获取新连接 - 知道客户端的ip地址和端口号
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾
      // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员
      int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);
      if (sockfd < 0)
      {
            // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人
            lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
            continue; // 所以这里使用continue
      }
      lg(Info, "get a new link..., sockfd: %d", sockfd);

      // 2.根据新连接来进行通信
      // 获取客户端的ip地址和端口号
      uint16_t clientport = ntohs(client.sin_port);
      char clientip;
      // 转化成主机序列
      inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
      cout << "clientport: " << clientport << ", clientip: " << clientip << endl;
      // 单进程版本
      // Service(sockfd, clientip, clientport);
      // close(sockfd);

      // 多进程版本
      pid_t id = fork();
      if(id == 0)
      {
            // 子进程
            // 子进程会继承父进程的文件描述符
            close(_listensocket);
            if(fork() > 0) exit(0); // 此时子进程退出了
            Service(sockfd, clientip, clientport); // 孙子进程执行
            // 对于孙子进程,它的父进程已经退出了,此时孙子进程被系统领养
            close(sockfd);
            exit(0);
      }
      // 父进程
      // 文件描述符使用的是引用计数
      // 关闭父进程的文件描述符不会影响子进程
      close(sockfd);
      // 这里等待回收子进程的方式不能是阻塞等待
      pid_t rid = waitpid(id, nullptr, 0);
    }
} 此时我们再来测试一下:
https://img-blog.csdnimg.cn/direct/5833aaba04284081beda0d9f6c243d23.png
此时我们无论来多少个客户端,我们的服务器都能解决!!!除了上面一种方法,我们还可以利用信号的方式,注意利用这个必要带上头文件<signal.h>
signal(SIGVHLD, SIG_IGN); 如果我们此时来了一大批客户,那么此时就会为每一个客户创建一个进程,而我们之前学过,创建子进程的本钱是非常大的,所以此时我们可以利用多线程来解决。
#pragma onec

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include "log.hpp"
using namespace std;

const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
Log lg;

enum
{
    UsageError = 1,
    SocketError = 2,
    BindError = 3,
    ListenError = 4
};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t)
    : sockfd(fd)
    , clientip(ip)
    , clientport(p)
    , tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};


class TcpServer
{
public:
    TcpServer(const uint16_t &port, const string &ip = defaultip)
      : _listensocket(defaultsockfd), _port(port), _ip(ip)
    {
    }
    void Init()
    {
      // 1.创建套接字
      _listensocket = socket(AF_INET, SOCK_STREAM, 0);
      if (_listensocket < 0) // 创建套接字失败
      {
            lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
      }
      lg(Info, "create socket success, listensocket: %d", _listensocket);

      // 2.绑定端口号
      //使用这个结构体需要包头文件
      struct sockaddr_in local;
      memset(&local, 0, sizeof(local));
      local.sin_family = AF_INET;
      // 转化为网络序列
      local.sin_port = htons(_port);
      // 字符串转化为点时分形式的ip
      inet_aton(_ip.c_str(), &(local.sin_addr));
      // local.sin_addr.s_addr = INADDR_ANY;
      // 此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
      int n = bind(_listensocket, (const struct sockaddr *)&local, sizeof(local));
      if (n < 0)
      {
            lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
      }
      lg(Info, "bind socket success, listensocket: %d", _listensocket);

      // 3.设置监听状态
      // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
      if (listen(_listensocket, backlog) < 0)
      {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
      }
      lg(Info, "bind socket success, listensocket: %d", _listensocket);
    }

    void Start()
    {
      lg(Info, "tcpServer is running....");
      for (;;)
      {
            // 1. 获取新连接 - 知道客户端的ip地址和端口号
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾
            // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员
            int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;                                                                      // 所以这里使用continue
            }
            lg(Info, "get a new link..., sockfd: %d", sockfd);

            // 2.根据新连接来进行通信
            // 获取客户端的ip地址和端口号
            uint16_t clientport = ntohs(client.sin_port);
            char clientip;
            // 转化成主机序列
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            cout << "clientport: " << clientport << ", clientip: " << clientip << endl;
            // 单进程版本
            // Service(sockfd, clientip, clientport);
            // close(sockfd);

            // 多进程版本
            // pid_t id = fork();
            // if(id == 0)
            //{
                // 子进程
                // 子进程会继承父进程的文件描述符
                // close(_listensocket);
                // if(fork() > 0) exit(0); // 此时子进程退出了
                // Service(sockfd, clientip, clientport); // 孙子进程执行
                // 对于孙子进程,它的父进程已经退出了,此时孙子进程被系统领养
                // close(sockfd);
                // exit(0);
            //}
            // 父进程
            // 文件描述符使用的是引用计数
            // 关闭父进程的文件描述符不会影响子进程
            // close(sockfd);
            // 这里等待回收子进程的方式不能是阻塞等待
            // pid_t rid = waitpid(id, nullptr, 0);

            // 多线程版本
            ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Rountine, td);

            // 这里不用join,因为它是阻塞等待
            // pthread_join(tid, nullptr);

      }
    }

    static void* Rountine(void* args)
    {
      pthread_detach(pthread_self()); // 设置分离状态
      // 文件描述符共享,此时我们就不能关闭
      // 多线程只拥有tcb
      ThreadData *td = static_cast<ThreadData *>(args);
      // static静态成员方法无法使用非静态成员方法和成员
      // 1.将Service放到TcpServer类外
      // 2.将当前对象的this指针传入
      td->tsvr->Service(td->sockfd, td->clientip, td->clientport);
      delete td;
      return nullptr;
    }

    void Service(int sockfd, const string &ip, const uint16_t &port)
    {
      char buffer;
      while (true)
      {
            // 读取用户发送的请求
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer = '\0';
                cout << "client says: " << buffer << endl;
                string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                // 此时需要关闭文件描述符
                lg(Info, "%s:%d quit, server close sockfd: %d", ip.c_str(), port, sockfd);
                break;
            }
            else
            {
                lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));
                break;
            }
      }
    }
    ~TcpServer()
    {
      close(_listensocket);
    }

private:
    int _listensocket; // 套接字
    uint16_t _port;    // 端口号
    string _ip;      // ip地址
}; 此时我们再来运行一下:
https://img-blog.csdnimg.cn/direct/0147e9144e434da9add589d7585f63bc.png
此时我们是每次来一个客户端,然后就创建一个线程,也有点消耗,毕竟俺们每次都要创建,我们来写一个线程池,一开始我们就创建一大批进程,来一个客户端直接给它一个线程。并且我们上面的代码还存在一个问题,我们的服务是一直服务用户请求的,只要客户端不退,即使客户端不发信息,我们为该用户创建的线程也不会销毁,如许就会导致系统中的线程越来越多,直接来写代码。
#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 10;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
      pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
      pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
      pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
      pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
      return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
      for (const auto &ti : threads_)
      {
            if (ti.tid == tid)
                return ti.name;
      }
      return "None";
    }

public:
    static void *HandlerTask(void *args)
    {
      ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
      std::string name = tp->GetThreadName(pthread_self());
      while (true)
      {
            tp->Lock();

            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t();
      }
    }
    void Start()
    {
      int num = threads_.size();
      for (int i = 0; i < num; i++)
      {
            threads_.name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_.tid), nullptr, HandlerTask, this);
      }
    }
    T Pop()
    {
      T t = tasks_.front();
      tasks_.pop();
      return t;
    }
    void Push(const T &t)
    {
      Lock();
      tasks_.push(t);
      Wakeup();
      Unlock();
    }
    static ThreadPool<T> *GetInstance()
    {
      if (nullptr == tp_) // ???
      {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
      }

      return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
      pthread_mutex_init(&mutex_, nullptr);
      pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
      pthread_mutex_destroy(&mutex_);
      pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER; 我们上面实现的线程池是一个单例模式,只允许创建一个对象,并且我们的线程池可以发送任务,它可以处置惩罚好,所以我们可以把服务器接收到的任务给我们的线程池,所以我们再来写一个任务。
#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include <string.h>

extern Log lg;
using namespace std;

class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
      : sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
    {
    }
    void run()
    {
      char buffer;

      // 读取用户发送的请求
      ssize_t n = read(sockfd_, buffer, sizeof(buffer));
      if (n > 0)
      {
            buffer = '\0';
            cout << "client says: " << buffer << endl;
            string echo_string = "tcpserver echo# ";
            echo_string += buffer;

            write(sockfd_, echo_string.c_str(), echo_string.size());
      }
      else if (n == 0)
      {
            // 此时需要关闭文件描述符
            lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
      }
      else
      {
            lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));
      }
      close(sockfd_);
    }
    void operator()()
    {
      run();
    }
    ~Task()
    {
    }

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
}; 此时对于客户端,我们让它输入一次信息就不要再输入了,你要是再输入就必要重新毗连服务器。
#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>

using namespace std;
void Usage(const string &proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n"
         << endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(1);
    }
    string serverip = argv;
    uint16_t serverport = stoi(argv);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
      cerr << "socket error" << endl;
    }

    // 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
    // tcp客户端要不要bind?一定要显示绑定只不过不需要用户显示的bind!一般有OS自由随机选择!
    // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    // 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
    // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
    // 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
    // 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
    // 系统什么时候给我bind呢?首次发送connect的时候,进行自动随机绑定

    // 可是客户端此时不知道服务器的ip和端口号
    // 使用命令行参数来解决
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    int n = connect(sockfd, (const struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
      cerr << "connect error..." << endl;
      exit(2);
    }

    string message;
   
   
      cout << "Please Enter# ";
      getline(cin, message);
      // 发送数据
      n = write(sockfd, message.c_str(), message.size());
      // 读取数据
      char inbuffer;
      n = read(sockfd, inbuffer, sizeof(inbuffer));
      if (n > 0)
      {
            inbuffer = 0;
            std::cout << inbuffer << std::endl;
      }
   

    close(sockfd);
    return 0;
} 此时我们就不必要再创建线程了,并且用户发一次信息,服务处置惩罚完该线程就退了,用户如果还有就必要重新毗连服务器。
https://img-blog.csdnimg.cn/direct/42841151ad2f4ea18c5b8b8463cb9fb9.png
3.服务端写入失败,写入必要判断

https://img-blog.csdnimg.cn/direct/c10f17fd427444c5b46b809970890c2c.png

此时我们再来看看结果:
https://img-blog.csdnimg.cn/direct/4864647237654dd187c6ec15b9a06529.png
此时就符合我们的预期结果啦!
4.服务器未写,sockfd链接断开

当我们的服务器已经读到数据的时候,但是此时文件描述符的链接一键断开,此时再向这个失效的文件描述符写那么步伐就会出问题,这个就相当于之前的管道,如果读端关掉,那么写端继续写当前举行就会收到SIGPIPE信号,将进程终止掉,此时步伐就会出现SIGPIPE信号,我们直接将其直接终止。
https://img-blog.csdnimg.cn/direct/729fe5e555ad494bbd5376929e6b724d.png
5.客户端多次链接,服务器提供多次服务

https://img-blog.csdnimg.cn/direct/c056a4183b6b4bca90aa8c44f4df55ee.png
然后我们来测试一下:
https://img-blog.csdnimg.cn/direct/27c6d45ed46e4779b5e488dd072a7a4e.png
这里我们再次链接的到时候为什么失败了呢?这是因为我们第一次发出请求的时候,服务器处置惩罚完了之后就将文件描述符关掉了,而链接的时候利用的文件描述符已经被关了,此时会链接失败,所以要想再次链接,就必须要再次创建套接字。
https://img-blog.csdnimg.cn/direct/8bb50a2338db46a1adc0bff0b2c71921.png
此时就解决问题啦!
6.客户端未发送,服务器断开

当客户端向服务器写的时候,此时服务器断开了,此时我们渴望能够重新毗连服务器,如果重连5次还没有毗连上,用户也离线。
#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>

using namespace std;
void Usage(const string &proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n"
         << endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(1);
    }
    string serverip = argv;
    uint16_t serverport = stoi(argv);

    // 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
    // tcp客户端要不要bind?一定要显示绑定只不过不需要用户显示的bind!一般有OS自由随机选择!
    // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    // 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
    // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
    // 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
    // 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
    // 系统什么时候给我bind呢?首次发送connect的时候,进行自动随机绑定

    // 可是客户端此时不知道服务器的ip和端口号
    // 使用命令行参数来解决
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true)
    {
      int cnt = 5; // 重连的次数
      bool isreconnect = false;
      int sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0)
      {
            cerr << "socket error" << endl;
      }
      do
      {
            int n = connect(sockfd, (const struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
                close(sockfd);
                sleep(2);
            }
            else
            {
                break;
            }
      } while (cnt && isreconnect);
      if (cnt == 0)
      {
            std::cerr << "user offline..." << std::endl;
            break;
      }

      string message;

      cout << "Please Enter# ";
      getline(cin, message);
      // 发送数据
      n = write(sockfd, message.c_str(), message.size());
      if (n < 0)
      {
            isreconnect = true;
            std::cerr << "write error..." << std::endl;
            continue;
      }
      // 读取数据
      char inbuffer;
      n = read(sockfd, inbuffer, sizeof(inbuffer));
      if (n > 0)
      {
            inbuffer = 0;
            std::cout << inbuffer << std::endl;
      }
      close(sockfd);
    }

    return 0;
} 运行结果:
https://img-blog.csdnimg.cn/direct/ecda8e82781d4e6a92919e7cc0ae727a.png
上面这个情况就是我们打游戏的时候,我们的网断开了,就相当于毗连不上服务器了,此时就会多次毗连,如果毗连了很多次都没有毗连上,此时游戏也就会退出啦。
五、英译汉服务器

   makefile
.PHONY:all
all:tcpserver tcpclient
tcpserver:Main.cc
        g++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.cc
        g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
        rm -f tcpserver tcpclient   dict.txt
apple:苹果...
banana:香蕉...
red:红色...
yellow:黄色...
the: 这
be: 是
to: 朝向/给/对
and: 和
I: 我
in: 在...里
that: 那个
have: 有
will: 将
for: 为了
but: 但是
as: 像...一样
what: 什么
so: 因此
he: 他
her: 她
his: 他的
they: 他们
we: 我们
their: 他们的
his: 它的
with: 和...一起
she: 她
he: 他(宾格)
it: 它   Main.cc
#include "TcpServer.hpp"

#include <memory>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port\n" << std::endl;
}

// ./tcpserver 8080
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
      Usage(argv);
      exit(UsageError);
    }
    uint16_t port = std::stoi(argv);
    unique_ptr<TcpServer> tcpSvr(new TcpServer(port));
    tcpSvr->Init();
    tcpSvr->Start();
    return 0;
}   Task.hpp
#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include "Init.hpp"
#include <string.h>

extern Log lg;
Init init;
using namespace std;

class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
      : sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
    {
    }
    void run()
    {
      char buffer;

      // 读取用户发送的请求
      ssize_t n = read(sockfd_, buffer, sizeof(buffer));
      if (n > 0)
      {
            buffer = '\0';
            cout << "client keys: " << buffer << endl;
            string echo_string = init.translation(buffer);
            int n = write(sockfd_, echo_string.c_str(), echo_string.size());
            if(n < 0)
            {
                lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
            }
      }
      else if (n == 0)
      {
            // 此时需要关闭文件描述符
            lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
      }
      else
      {
            lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));
      }
      close(sockfd_);
    }
    void operator()()
    {
      run();
    }
    ~Task()
    {
    }

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};   Init.pp
#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "log.hpp"

Log lg;

const std::string dictname = "./dict.txt";
const std::string sep = ":";

static bool Split(std::string &s, std::string *part1, std::string *part2)
{
    auto pos = s.find(sep);
    if (pos == std::string::npos)
      return false;
    *part1 = s.substr(0, pos);
    *part2 = s.substr(pos + 1);
    return true;
}

class Init
{
public:
    Init()
    {
      std::ifstream in(dictname); // 打开配置文件
      if (!in.is_open())          // 打开配置文件失败
      {
            lg(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
      }
      std::string line;
      while (std::getline(in, line))
      {
            std::string part1, part2;
            Split(line, &part1, &part2);
            dict.insert({part1, part2});
      }
      in.close();
    }
    std::string translation(const std::string &key)
    {
      auto iter = dict.find(key);
      if (iter == dict.end())
            return "Unknow";
      else
            return iter->second;
    }

private:
    std::unordered_map<std::string, std::string> dict;
};   log.hpp
#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include "Init.hpp"
#include <string.h>

extern Log lg;
Init init;
using namespace std;

class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
      : sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
    {
    }
    void run()
    {
      char buffer;

      // 读取用户发送的请求
      ssize_t n = read(sockfd_, buffer, sizeof(buffer));
      if (n > 0)
      {
            buffer = '\0';
            cout << "client keys: " << buffer << endl;
            string echo_string = init.translation(buffer);
            int n = write(sockfd_, echo_string.c_str(), echo_string.size());
            if(n < 0)
            {
                lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
            }
      }
      else if (n == 0)
      {
            // 此时需要关闭文件描述符
            lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
      }
      else
      {
            lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));
      }
      close(sockfd_);
    }
    void operator()()
    {
      run();
    }
    ~Task()
    {
    }

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};   TcpServer.hpp
#pragma onec

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
using namespace std;

const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
extern Log lg;

enum
{
    UsageError = 1,
    SocketError = 2,
    BindError = 3,
    ListenError = 4
};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t)
    : sockfd(fd)
    , clientip(ip)
    , clientport(p)
    , tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};


class TcpServer
{
public:
    TcpServer(const uint16_t &port, const string &ip = defaultip)
      : _listensocket(defaultsockfd), _port(port), _ip(ip)
    {
    }
    void Init()
    {
      // 1.创建套接字
      _listensocket = socket(AF_INET, SOCK_STREAM, 0);
      if (_listensocket < 0) // 创建套接字失败
      {
            lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
      }
      lg(Info, "create socket success, listensocket: %d", _listensocket);

      // 2.绑定端口号
      //使用这个结构体需要包头文件
      struct sockaddr_in local;
      memset(&local, 0, sizeof(local));
      local.sin_family = AF_INET;
      // 转化为网络序列
      local.sin_port = htons(_port);
      // 字符串转化为点时分形式的ip
      inet_aton(_ip.c_str(), &(local.sin_addr));
      // local.sin_addr.s_addr = INADDR_ANY;
      // 此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
      int n = bind(_listensocket, (const struct sockaddr *)&local, sizeof(local));
      if (n < 0)
      {
            lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
      }
      lg(Info, "bind socket success, listensocket: %d", _listensocket);

      // 3.设置监听状态
      // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
      if (listen(_listensocket, backlog) < 0)
      {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
      }
      lg(Info, "bind socket success, listensocket: %d", _listensocket);
    }

    void Start()
    {
      signal(SIGPIPE, SIG_IGN);
      ThreadPool<Task>::GetInstance()->Start();
      lg(Info, "tcpServer is running....");
      for (;;)
      {
            // 1. 获取新连接 - 知道客户端的ip地址和端口号
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾
            // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员
            int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;                                                                      // 所以这里使用continue
            }
            lg(Info, "get a new link..., sockfd: %d", sockfd);

            // 2.根据新连接来进行通信
            // 获取客户端的ip地址和端口号
            uint16_t clientport = ntohs(client.sin_port);
            char clientip;
            // 转化成主机序列
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            cout << "clientport: " << clientport << ", clientip: " << clientip << endl;
            // 单进程版本
            // Service(sockfd, clientip, clientport);
            // close(sockfd);

            // 多进程版本
            // pid_t id = fork();
            // if(id == 0)
            //{
                // 子进程
                // 子进程会继承父进程的文件描述符
                // close(_listensocket);
                // if(fork() > 0) exit(0); // 此时子进程退出了
                // Service(sockfd, clientip, clientport); // 孙子进程执行
                // 对于孙子进程,它的父进程已经退出了,此时孙子进程被系统领养
                // close(sockfd);
                // exit(0);
            //}
            // 父进程
            // 文件描述符使用的是引用计数
            // 关闭父进程的文件描述符不会影响子进程
            // close(sockfd);
            // 这里等待回收子进程的方式不能是阻塞等待
            // pid_t rid = waitpid(id, nullptr, 0);

            // 多线程版本
            // ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);
            // pthread_t tid;
            // pthread_create(&tid, nullptr, Rountine, td);
            // 这里不用join,因为它是阻塞等待
            // pthread_join(tid, nullptr);

            // 线程池版本
            Task t(sockfd, clientip, clientport);
            // 单例模式
            ThreadPool<Task>::GetInstance()->Push(t);
      }
    }

    //static void* Rountine(void* args)
    //{
       // pthread_detach(pthread_self()); // 设置分离状态
      // 文件描述符共享,此时我们就不能关闭
      // 多线程只拥有tcb
      //ThreadData *td = static_cast<ThreadData *>(args);
      // static静态成员方法无法使用非静态成员方法和成员
      // 1.将Service放到TcpServer类外
      // 2.将当前对象的this指针传入
      //td->tsvr->Service(td->sockfd, td->clientip, td->clientport);
      //delete td;
      //return nullptr;
    //}

    ~TcpServer()
    {
      close(_listensocket);
    }

private:
    int _listensocket; // 套接字
    uint16_t _port;    // 端口号
    string _ip;      // ip地址
};   TcpClient.cc
#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>

using namespace std;
void Usage(const string &proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n"
         << endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(1);
    }
    string serverip = argv;
    uint16_t serverport = stoi(argv);

    // 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
    // tcp客户端要不要bind?一定要显示绑定只不过不需要用户显示的bind!一般有OS自由随机选择!
    // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    // 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
    // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
    // 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
    // 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
    // 系统什么时候给我bind呢?首次发送connect的时候,进行自动随机绑定

    // 可是客户端此时不知道服务器的ip和端口号
    // 使用命令行参数来解决
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true)
    {
      int cnt = 5; // 重连的次数
      bool isreconnect = false;
      int sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0)
      {
            cerr << "socket error" << endl;
      }
      do
      {
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
                sleep(2);
            }
            else
            {
                break;
            }
      } while (cnt && isreconnect);
      if (cnt == 0)
      {
            std::cerr << "user offline..." << std::endl;
            break;
      }

      string message;

      cout << "Please Enter# ";
      getline(cin, message);
      // 发送数据
      int n = write(sockfd, message.c_str(), message.size());
      if (n < 0)
      {
            isreconnect = true;
            std::cerr << "write error..." << std::endl;
            continue;
      }
      // 读取数据
      char inbuffer;
      n = read(sockfd, inbuffer, sizeof(inbuffer));
      if (n > 0)
      {
            inbuffer = 0;
            std::cout << inbuffer << std::endl;
      }
      close(sockfd);
    }

    return 0;
} 服务端:
https://img-blog.csdnimg.cn/direct/dbd93617ddcd47fa94905be350f1e7cc.png
客户端:
https://img-blog.csdnimg.cn/direct/3b1387ea39d24acc9cf7196f6b833496.png
然后我们再来测试一下如果服务器断开了我们还能不能重新连上。
https://img-blog.csdnimg.cn/direct/df6a19e93c16424eb756708b92325354.png
我们发现此时不能重现毗连乐成,这是因为我们的端口号不能重现启动,所以我们要在服务器端加两句代码。
int opt = 1;
setsockopt(_listensocket, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
// 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说) 此时我们来看看运行结果:
https://img-blog.csdnimg.cn/direct/e93f1a1ce47341f9bd8447b56e772bdd.png
六、前台和后台进程

可是万一有一天我们不警惕将我们的xshell关掉了呢?此时服务器就断开了,我们想xshell关掉了服务器依然能跑,要做到这个,我们要理解前台进程和后台进程,先来测试一下它们的特点。
#include <iostream>
#include <string>
#include <unistd.h>

int main()
{
    while(true)
    {
      std::cout << "hello ...." << std::endl;
      sleep(1);
    }
    return 0;
} 前台进程:
https://img-blog.csdnimg.cn/direct/3e0256a04b264d8f9a9a0faebb70e733.png
后台进程:
https://img-blog.csdnimg.cn/direct/2f70cc74bcd34d25b61c9de31661bd80.png

前台进程:

[*]直接交互:前台进程直接与用户交互,意味着用户可以通过命令行与这些进程举行输入和输出操作。
[*]终端阻塞:当一个进程在前台运行时,它通常会阻塞用户终端,直到该进程完成或被挂起。这意味着用户不能在同一终端启动其他必要交互的进程。
[*]数目限制:在一个会话中,同时只能有一个进程组在前台运行,只管这个进程组内可能包含多个进程。
后台进程:

[*]非交互式运行:后台进程在后台运行,不直接与用户交互,即利用户没有自动与其互动,也能连续实行任务。
[*]不阻塞终端:用户可以在后台进程运行的同时,在同一个终端上实行其他命令或启动其他进程,因为它不阻塞用户界面。
[*]启动方式:可以通过在命令末端添加符号&来将进程置于后台启动。
https://img-blog.csdnimg.cn/direct/2206768499d240618a3e378ed7d42d0b.png
此时我们可以把进程运行的结果重定向到文件中,如果交给我们的前台进程,那么此时只能实行一个写入文件操作,因为我们的前台进程只有一个,但是后台进程有多个,我们可以交给后台进程,并且还能通过jobs来查看后台进程。
https://img-blog.csdnimg.cn/direct/a9563ea44e1b410cbe3cbb88215f36dd.png
如果我们向终止任务,可以利用fg 任务号将这个任务提到前台进程,但是此时bash就会变成后台进程,然后将它干掉即可。如果我们不想干掉,想重新仍会后台进程呢?
https://img-blog.csdnimg.cn/direct/e0c1cec3f5934fd4a004b48c72d48ab3.png
此时我们停息前台进程,系统要不要把bash提到前台进程,把停息的进程提到后台进程,要的,bash必须变成前台进程,所以在命令行中,前台进程一定存在。
https://img-blog.csdnimg.cn/direct/ebee3a5504024eaab42c353076a5c3ef.png
随后我们再来理解一下后台进程。
https://img-blog.csdnimg.cn/direct/de0b26d35d0e4934a9d78e0f8d620a9f.png
每次登录的时候,我们的session都是差别的,所以session id也是差别的。
https://img-blog.csdnimg.cn/direct/a3280ea7eb2845b78d198903b16624cd.png
这里我们在开启两个终端。
https://img-blog.csdnimg.cn/direct/0ad5658381114a19a0e8adccd1319a84.png
如果我们的前台进程退了,开启的后台进程呢?它会自己退嘛?
https://img-blog.csdnimg.cn/direct/f60877e204594c1096a2061e07674812.png
此时我们发现后台进程也退出了,所以用户在退出的时候,会将自己启动的所有进程关掉,这就是注销,如果我们不想让我们的后台进程不随任何用户的登录大概退出受影响,此时我们就要保卫进程化,我们将自成session自成进程组的进程,称为保卫进程。
https://img-blog.csdnimg.cn/direct/dcf412a682ea482281837c283e6f4a1a.png
注意:如果调用进程不是一个进程组组长,则创建一个新的会话,但是我们怎么保证不是进程组组长的呢?我们可以创建一个子进程,然后父进程退出,此时的子进程就不是一个进程组组长,则创建一个新的会话,所以保卫进程的本质,也是孤儿进程!但是该孤儿进程很刚强,它把自己设置成新的session,它就是一个独立的session,如许不从属于任何用户登录和注销的影响。
#pragma onec

#include <unistd.h>
#include <iostream>
#include <signal.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null";

// 让服务器调用还函数,以守护进程的形式进行运行
void Daemon(const std::string &cwd = "")
{
    // 1.忽略其他异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2.将自己变成独立的会话
    // 父进程退出
    if(fork() > 0) exit(0);
    setsid();

    // 3. 更改当前调用进程的工作目录
    if (!cwd.empty())
      chdir(cwd.c_str());

    // 4.关闭? 标准输入,标准输出,标准错误
    // 但是如果我们关闭了,那此时日志里面的打印全都会出错
    // 系统为我们提供了一个/dev/null文件,它像一个垃圾桶
    // 凡是向/dev/null文件里面写入的信息全部都会被丢弃
    // 4.标准输入,标准输出,标准错误 -> 重定向到/dev/null文件
    // 此时日志的信息都会丢弃,那么我们看不到错误的信息了嘛?是滴
    // 所以日志的信息我们可以将它写到文件中,反正不应该出现在显示器文件
    int fd = open(nullfile.c_str(), O_RDWR); // 读写方式打开
    if(fd > 0) // 打开成功
    {
      dup2(fd, 0);
      dup2(fd, 1);
      dup2(fd, 2);
      close(fd);
    }
} 随后我们将保卫进程的代码加入服务器启动的最开始的地方。
https://img-blog.csdnimg.cn/direct/238cc56b269e4bfcb0de1020186615b9.png
随后我们开始运行一下:
https://img-blog.csdnimg.cn/direct/e43152c2b62f4f2f949a95ef14c41d7d.png
并且此时我们能够确定我们的服务器的进程不从属于bash的session,而是单成session。
https://img-blog.csdnimg.cn/direct/edcd77439f774da6bfc36fd6a671baa2.png
bash的session id和我们的服务器的session id不一样哦!
https://img-blog.csdnimg.cn/direct/831dadd035414b998e574f266d10d79c.png
此时我们能够发现我们已经将日志重定向到垃圾桶啦!随后我们再将我们的xshell关掉,然后我们再打开xshell,运行我们的服务器,看看结果:
https://img-blog.csdnimg.cn/direct/e5317ebe941d4357a806afa721ab4d7c.png
此时只要我们一运行我们的客户端,就可以访问,真正做到了24小时提供服务,那我们怎么关掉我们的服务器呢?直接kill就行啦!!!
https://img-blog.csdnimg.cn/direct/dd31bcc415c4499993ae85c6fa2cb1aa.png
注意:为了标识保卫进程,我们一样平常给文件名为末端加上d。
如果我们想保留日志文件,那么我们就要传入写入的方式。
https://img-blog.csdnimg.cn/direct/b50206f729324afa9121dbe5c37653d1.png
https://img-blog.csdnimg.cn/direct/dd882d25c5ab40678668e39d9e2cde2a.png
此时我们就能在日志里面看到该日志文件,但是保卫进程还是太麻烦了,要自己来写,其实系统也为我们实现了。
https://img-blog.csdnimg.cn/direct/95d273e50d234de5bb5cc0b32a864a3a.png
这个函数接受两个整型参数:
   
[*] nochdir:

[*]如果 nochdir 参数为0,daemon() 函数将会把当前工作目录更改为根目录("/")。这是保卫进程的标准行为,避免因当前工作目录被卸载而导致的问题。
[*]如果 nochdir 为非0值,则不改变当前工作目录。

[*] noclose:

[*]如果 noclose 参数为0,daemon() 函数会关闭标准输入、标准输出和标准错误,并将它们都重定向到 /dev/null。这可以防止保卫进程因为试图写入终端而阻塞或产生不必要的输出。
[*]如果 noclose 为非0值,标准输入、输出和错误保持不变。但通常情况下,为了确保保卫进程的无终端运行,我们会选择关闭它们。

利用 daemon() 函数的根本步调通常包括:


[*]调用 fork() 创建子进程,父进程退出,如许新进程就不再与终端关联。
[*]在子进程中调用 setsid() 成为新的会话向导并脱离控制终端。
[*]调用 umask() 设置符合的权限掩码。
[*]根据必要调用 chdir("/") 更改当前工作目录到根目录。
[*]重定向标准输入、输出和错误流,大概通过 daemon() 函数自动处置惩罚。
[*]继续实行保卫进程的详细任务。
七、TCP协议通讯流程

下图是基于TCP协议的客户端/服务器步伐的一样平常流程:
https://img-blog.csdnimg.cn/direct/d32a70ebd03344938966cb21bfe42436.png
服务器初始化:


[*]调用socket, 创建文件描述符;
[*]调用bind, 将当前的文件描述符和ip/port绑定在一起;
[*]如果这个端口已经被其他进程占用了, 就会bind失败;
[*]调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备; 调用accecpt, 并阻塞, 等待客户端毗连过来;
建立毗连的过程:


[*]调用socket, 创建文件描述符;
[*]调用connect, 向服务器发起毗连请求;
[*]connect会发出SYN段并阻塞等待服务器应答;
[*](第一次) 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立毗连"; (
[*]第二次) 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立毗连的过程, 通常称为 三次握手;
数据传输的过程


[*]建立毗连后,TCP协议提供全双工的通讯服务;
[*]所谓全双工的意思是, 在同一条毗连中, 同一时候, 通讯两边 可以同时写数据;
[*]相对的概念叫做半双工, 同一条毗连在同一时候, 只能由一方来写数据;
[*]服务器从accept()返回后立即调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
[*]这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求举行处置惩罚, 在此期 间客户端调用read()阻塞等待服务器的应答;
[*]服务器调用write()将处置惩罚结果发回给客户端, 再次调用read()阻塞等待下一条请求;
[*]客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开毗连的过程:


[*]如果客户端没有更多的请求了, 就调用close()关闭毗连, 客户端会向服务器发送FIN段(第一次);
[*]此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
[*]read返回之后, 服务器就知道客户端关闭了毗连, 也调用close关闭毗连, 这个时候服务器会向客户端发送 一个FIN;(第三次)
[*]客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开毗连的过程, 通常称为 四次挥手
在学习socket API时要注意应用步伐和TCP协议层是如何交互的:


[*]应用步伐调用某个socket函数时TCP协议层完成什么动作,好比调用connect()会发出SYN段
[*]应用步伐如何知道TCP协议层的状态变化,好比从某个阻塞的socket函数返回就表明TCP协议收到了某些 段,再好比read()返回0就表明收到了FIN段
谈恋爱例子
https://img-blog.csdnimg.cn/direct/5c0f44046c2b4209836e43c99154db3c.png
八、TCP全双工通讯

全双工通讯:


[*]在全双工模式下,数据可以同时在两个方向上传输,即通讯的两边能够同时举行发送和接收操作,互不影响。这就像两个人在打电话,两边可以同时说话和凝听,无需等待对方说完再回应。
半双工通讯:


[*]半双工模式允许数据在两个方向上传输,但不能同时举行。这意味着在任何给定的时间点,数据只能在一个方向流动。一旦一方开始发送数据,另一方就必须停止发送并转为接收模式,直到前一方发送完毕。这种模式类似于对讲机,利用者必须等待对方讲完并说“Over”后,才可开始自己的发言。
我们为什么要讲这个呢?因为我们的tcp是全双工通讯的,它是如何做到的呢?
https://img-blog.csdnimg.cn/direct/4080589337b543118788b4515f4536f1.png
每个TCP毗连都有独立的发送缓冲区和接收缓冲区。这意味着一个端点可以在其发送缓冲区列队待发送的数据,同时从接收缓冲区读取对方发送过来的数据。这两个操作可以并发举行,从而实现了数据的双向同时传输,所以未来我们可以对一个套接字多进程的并发的读和写,但是两个线程不能同时读和同时写。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 简单的TCP网络步伐:英译汉服务器