小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【linux】网络套接字编程(三)UDP服务器与客户端实现:跨主机执行命令程序,windows与linux通信执行命令程序,多人在线聊天程序,inet_ntop——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】网络套接字编程(四)TCP服务器与客户端的实现(单进程/单线程的TCP服务器),setsockopt,listen,accept,telnet,connect,inet_pton


一、TCP服务器TcpServer.hpp(版本一:单进程/单线程版)

  1. 关于TCP协议的介绍,以及socket,bind,htons,ntohs等接口的使用,端口号bind的介绍,小编已经在后方蓝字链接文章中进行了讲解 详情请点击<——
  2. 关于IP地址的讲解 详情请点击<——
  3. 关于日志的实现与基本使用讲解 详情请点击<——
  4. 上述的文章与TCP服务器的编写具有一定的铺垫作用,所以希望读者友友先学习上述文章之后再来进入本文的学习更加轻松
  5. 那么对于TCP的服务器,我们希望进行封装为TcpServer.hpp,并且在包含main函数中的文件中包TCP服务器的头文件TcpServer.hpp进行调用,所以下面我们就来先实现一下TCP服务器
  6. 其实TCP服务器的编写套路和UDP服务器的编写套路十分的类似,尤其是在初始化InitServer部分,几乎就是UDP服务器一样,所以本文会基于UDP服务器的实现的基础上直接进行简要的讲解,UDP服务器与客户端的实现,详情请点击<——

基本框架

  1. 首先小编在日志中直接定义了一个日志的全局变量Log lg,这样其它的文件如果想要使用日志,那么仅需要包含日志的头文件#include “Log.hpp”,然后extern声明外部变量即可使用日志
    在这里插入图片描述

  2. 那么接下来就是定义IP地址,端口号port,网络文件描述符fd的缺省参数,这里小编还定义了一个backlog为10,这个backlog用于给listen进行传参,关于listen是什么,小编后面会进行讲解

  3. 接下来使用枚举enum定义错误码,然后就可以开始TcpServer服务器的编写了

  4. 首先我们需要了解一下一个TCP服务器的类TcpServer中应该包含什么成员变量,那么TCP服务器的成员变量类似于UDP服务器的成员变量,即TCP服务器的成员变量需要包含网络文件描述符,端口号,IP地址,但是注意,这里小编关于类中定义的网络文件描述符的命名不是和UDP服务器写的都是socket_了,在TCP服务器这里,我们将这个网络文件描述符命名为listensock_,所以为什么呢?

  5. 因为TCP协议是面向连接的,服务器需要先和客户端建立起连接才能进行通信,所以服务器如何和客户端建立连接呢?所以服务器在进行初始化部分的时候,要先创建套接字,绑定,接下来和UDP不一样的是,TCP服务器需要额外使用listen接口,将网络文件描述符listensock_设置为监听状态,即TCP服务器初始化创建的套接字返回的网络文件描述符listensock_仅仅用于监听的作用,即表示当前服务器可以接收外部客户端的连接请求

  6. 那么在构造函数我们就进行对应字段的设置,在析构函数,如果创建了网络文件描述符listensock_大于0,那么我们就close关闭这个网络文件描述符对应的文件对象

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

const int defaultfd = -1;
const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";
const int backlog = 10;
extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : listensock_(defaultfd), port_(port), ip_(ip)
    {}

    ~TcpServer()
    {
        if (listensock_ > 0)
            close(listensock_);
    }

private:
    int listensock_;
    uint16_t port_;
    std::string ip_;
};

InitServer

  1. 那么在初始化InitServer部分,我们要完成socket创建套接字,struct sockaddre_in本地属性初始化,bind套接字绑定,listen将网络文件描述符listensock_设置为监听状态,下面小编将依次进行讲解
  2. 创建套接字要使用socket,并且我们创建的是TCP网络套接字,TCP是一个使用IPv4地址的网络通信协议,所以socket的第一个参数选择协议家族(域)AF_INET,TCP是面向字节流进行通信的,所以选择协议类型为SOCK_STREAM,最后一个参数默认为0即可,socket,bind的使用,详情请点击<——,如果套接字创建失败,那么我们使用日志打印信息,然后终止服务器,如果成功了那么我们打印日志信息即可
setsockopt

在这里插入图片描述

  1. 其实服务器如果中断,然后立即重启,会触发偶发性的重启失败,端口号无法绑定的问题,所以这里我们在服务器的初始化的时候,使用setsockopt这个接口就可以有效避免这种情况,setsockopt是作用是设置网络套接字的选项,所以依次传入网络文件描述符listensock_,然后level就是选择协议层,这里我们选择套接字层SOL_SOCKET,接下来就需要传入要设置的选项,那么这里我们设置重新使用IP地址,重新使用端口号即可SO_REUSEADDR|SO_REUSEPORT,optval即要设计选项的值,那么我们在本地定义一个值为1的opt然后取地址传入即可&opt,紧接着是传入这个变量的大小,即sizeof(opt)
  2. 接下来就是进行struct sockaddre_in本地属性初始化,那么我们和UDP一样,进行对应字段的设置,然后将端口号和IP地址的主机字节序列转换为网络字节序列进行传入即可
  3. 接下来就是进行套接字的绑定bind,那么和UDP的套路一样,依次传入网络文件描述符listensock_,要绑定的字段,字段的大小即可,如果绑定失败bind则会返回一个小于0的数,所以我们判断一下,如果绑定失败,那么打印日志信息,然后终止服务器,如果绑定成功,那么打印日志信息即可
listen

在这里插入图片描述

  1. 那么接下来就是使用listen将一个网络文件描述符对应的网络套接字设置为监听状态,那么依次传入网络文件描述符listensock_,然后再传入backlog,其中关于这个backlog我们在基本框架已经定义出来了,默认我们设置为10,表示允许这个服务器同时可以监听连接请求的最大数量,一般我们将其设置为10,并且不会将其设置为很大
    在这里插入图片描述
  2. 如果listen失败bind则会返回一个小于0的数,所以我们判断一下,如果listen监听失败,那么打印日志信息,然后终止服务器,如果listen监听成功,那么打印日志信息即可
void InitServer()
{
    listensock_ = socket(AF_INET, SOCK_STREAM, 0);
    if (listensock_ < 0)
    {
        lg(Fatal, "create socket error, errno: %d, errstring: %s", errno, strerror(errno));
        exit(SocketError);
    }
    lg(Info, "create socket success, listensock_: %d", listensock_);

    //防止偶发性服务器无法立即重启
    int opt = 1;
    setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(port_);
    inet_aton(ip_.c_str(), &(server.sin_addr));
    socklen_t len = sizeof(server);

    if (bind(listensock_, (struct sockaddr *)&server, len) < 0)
    {
        lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
        exit(BindError);
    }
    lg(Info, "bind socket success, listensock_: %d", listensock_);

    if (listen(listensock_, backlog) < 0)
    {
        lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
        exit(ListenError);
    }
    lg(Info, "listen socket success, listensock_: %d", listensock_);
}

StartServer

  1. 这个版本是单进程/单线程版本的启动服务器,那么首先我们打印一个日志消息表示服务器已经启动
  2. 既然是服务器,那么服务器就要24小时的为客户端提供服务,即服务器要一直运行下去,所以这里服务器启动的核心逻辑就要基于死循环for(; ; )进行编写
  3. 之前再服务器初始化的时候我们使用listen将网络文件描述符listensock_对应的网络套接字设置为监听状态,可以监听服务器收到的连接请求,所以此时服务器和客户端建立连接了吗?没有,仅仅是服务器知道了有几个客户端想要连接自己这个服务器,那么如何进行连接呢?accept
accept

在这里插入图片描述

  1. 使用accept即可,那么依次传入网络文件描述符listensock_,并且注意这个accept函数和我们在UDP阶段使用的recvfrom的后两个参数是一样的,所以对于我们来讲上手使用这个accept就没难度了
  2. 那么在本地定义一个sockaddr_in类型的对象client,然后取地址进行类型转换为struct sockaddr*再传入,然后计算出大小传入即可,后续连接上的客户端的字段信息,例如IP地址,端口号等就都在这个client中了,所以此时就可以进行连接客户端和服务端了
    在这里插入图片描述
  3. 那么接下来重点来了,那么就是accept的返回值,如果连接失败了,那么就会返回一个小于0的数,那么我们进行判断即可,如果返回值小于0,那么就日志打印即可,然后再continue重新执行连接(毕竟着只是一个连接失败,但是不能因为仅仅一个连接失败就将服务器终止吧,所以服务器连接失败的时候,不能终止,而是应该继续进行下一个连接),如果连接成功,那么就会返回一个网络文件描述符sockfd,那么我们就可以使用这个网络文件描述符进行读写式的进行通信了,如何读,如何写?
  4. 那么就和文件操作一样,由于文件是基于字节流的,而TCP恰好也是基于字节流的,所以这里完全可以使用文件的read和write向文件描述符中进行读取或写入,大大降低了我们的学习成本,所以后续服务端想要和客户端进行通信,那么就需要使用到accept的返回值,即使用网络文件描述符sockfd
  5. 那么问题来了,TCP服务器初始化然后socket创建的套接字的返回值listensock_和这里accept连接的返回值sockfd都是网络文件描述符,那么两者的作用分别是什么呢?
    (1)listensock_是网络文件描述符,用于监听服务器收到的连接请求,一般一个TCP服务器只有一个listensock_
    (2)sockfd同样是网络文件描述符,它的作用是用于服务端想要和已经连接上的客户端进行通信,进行读写操作的桥梁,用于服务器和客户端进行网络通信的网络文件描述符,服务器可以和多个客户端进行连接,所以也就意味着sockfd可以有多个
  6. 接下来走到下面accept服务器连接客户端成功,那么客户端的信息,例如IP地址,端口号port的信息就都在类型为struct sockaddr_in的这个client对象中了,但是此时对应的字段信息仍然为网络字节序列,小编想要将IP地址以及端口号提取出来在本主机上进行使用,那么就要网络字节序列转主机字节序列
  7. 那么对于端口号,我们可以使用ntohs进行转换为主机字节序列,那么对于为整数的网络字节序列的IP地址, 我们首先要将网络字节序列转换为为整数的主机字节序列,然后再将其转换为点分十进制的字符串风格的IP地址,这里我们使用inet_ntop
  8. 这里小编将主机序列转换为网络字节序列,并且转换为点分十进制的字符串风格IP地址的时候,则使用inet_ntop进行转换 关于inet_ntop如何使用,在第三点的测试的第二点中进行的讲解,详情请点击<——,那么我们依次传入协议家族(IPv4对应的是AF_INET),struct sockaddr_in中的sin_addr的地址(由于sin_addr的成员就是整数类型的IP地址,所以本质上就是传入IP地址字段),自己维护的缓冲区serverip,缓冲区的大小即可
  9. 所以转化端口号和IP地址完成后,那么接下来我们就可以打印一下日志信息了,将网络文件描述符sockfd进行打印然后打印IP地址和端口号信息,并且这里我们可以猜测一下这里的网络文件描述符sockfd应该是4,因为3是listensock_,2是标准错误,1是标准输出,0是标准输出
  10. 然后我们接下来执行Server函数,Server函数是用于服务端处理数据,然后服务端和客户端进行通信的函数,这个小编后面会进行实现,那么让Server函数执行完毕之后,此时当前服务器和客户端的通信已经结束了,所以我们在服务端将这个网络文件描述符sockfd使用close关闭即可
  11. 这个版本是单进程/单线程版本的启动服务器,因为这里执行了Server之后,当前进程就去进行服务器和客户端的通信了,在处理通信的期间无法继续接收其它客户端的连接请求,只有在处理完成了通信之后,关闭文件描述符sockfd,才可以继续接收其它客户端的连接请求,才可以和其它客户端进行通信,所以我们编写的当前版本是单进程/单线程的TCP服务器
void StartServer()
{
    lg(Info, "tcpserver is running...");

    for (;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        
        int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
        if (sockfd < 0)
        {
            lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
            continue;
        }
        uint16_t clientport = ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

        lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", 
            sockfd, clientip, clientport);
        
        Service(sockfd, clientip, clientport);
        close(sockfd);
    }
}

Service

  1. 现在我们设想的场景是客户端一旦连接上服务器,那么客户端期望和服务器源源不断的进行通信,即由服务器提供长服务,所以这里我们就基于while(true)死循环基础上进行编码
  2. 有了网络文件描述符,并且TCP是基于字节流的,文件也是基于字节流的,所以可以使用read读接收数据,使用write进行写发送数据
  3. 那么此时服务端就要读取数据,首先定义一个缓冲区用于将读取上来的数据放到这个缓冲区中,服务端和客户端约定,相互之间传输的数据是字符串,所以此时当read结束,就将这个缓冲区当作字符串来处理,那么使用n来接收read的返回值,n是read读取的字节的个数
  4. 如果n等于0,那么说明此时客户端已经退出,客户端已经将写端关闭了,所以此时我们我们再进行读取已经没有意义了,因为根本没有数据了,所以此时我们先使用日志打印信息,然后break退出死循环,那么结束服务,之后Service执行完毕,就会close关闭sockfd网络文件描述符
  5. 如果n小于0,说明读取失败,此时有可能sockfd有误或者其它原因无法进行读取,所以此时我们日志打印信息,同样break退出死循环,那么结束服务,之后Service执行完毕,就会close关闭sockfd网络文件描述符
  6. 如果n大于0,那么说明此时读取数据成功,所以此时我们就可以对缓冲区进行处理了,由于文件中的字符串的结尾没有’\0’,所以这里我们c/c++语言中规定,字符串要以’\0’结尾,所以我们就在n位置处放一个0即可,即在字符串的结尾添加’\0’,接下来那么我服务端回显打印一下收到了客户端的什么消息即可,然后定义一个字符串echo_string,使用服务端的信息"tcpserver echo# "初始化,将服务端收到的字符串也进行添加上
  7. 即服务端将收到的字符串进行简单的处理之后,然后再write通过sockfd写回给客户端即可
  8. 注意,这里必须要对n == 0或者n < 0的情况进行处理,因为一旦服务器或者客户端,一方作为读端,另一方作为写端进行网络通信,如果有一端突然退出,不想通信了,那么此时也就意味着信道的读端或者写端关闭了一个
  9. 那么此时我们就以客户端作为写端,服务器作为读端为例进行讲解,写端关闭了,那么此时读端就无法读取到数据了,所以也就意味着读端没有意义存在了,所以如果操作系统检测到了对方的写端关闭,而自己这边的读端还在读取,所以操作系统认为自己这边的读端进行读取没有意义,是在浪费资源的,而操作系统绝对不允许在系统内任何一件浪费时间和空间的事情的,而服务器本质上也是一个进程,既然是进程就统一归操作系统管理,操作系统有终止进程的权限,那么操作系统就会直接终止该服务器进程
  10. 那么这一终止可不得了,你试想一下,直接将服务器给终止了,那么所有的客户端都无法连接上服务器了,如果今天的服务器是微信服务器呢?所以所有的手机电脑上的微信客户端都无法通信,支付了,这本质上是非常严重的,所以我们呢不期望服务器被终止,所以这里我们必须要对read的返回值n进行获取,并且操作,一旦我们接收了read的返回值,并且及时进行了操作,我们服务器及时主动的关闭这边的读端,那么此时操作系统就不会终止这个进程了,所以也就保障了服务器可以一直运行
void Service(int sockfd, const std::string& clientip, const uint16_t& clientport)
{
    while (true)
    {
        char inbuffer[4096];
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << "client say# " << inbuffer << std::endl;

            std::string echo_string = "tcpserver echo# ";
            echo_string += inbuffer;

            write(sockfd, echo_string.c_str(), echo_string.size());                
        }
        else if(n == 0)
        {
            lg(Info, "%s:%d quit, server colse sockfd: %d", clientip.c_str(), clientport, sockfd);
            break;
        }
        else  
        {
            //异常
            lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", 
                sockfd, clientip.c_str(), clientport);
            break;
        }
    }
}

二、Main.cc

  1. main函数的编写以及调用逻辑和UDP协议的main函数类似,即获取命令行参数,提取port,然后使用智能指针管理new出来的TCP服务器对象,给这个TCP服务器对象传参port,接下来初始化TCP服务器,然后运行TCP服务器即可
#include <iostream>
#include <memory>
#include "TcpServer.hpp"


void Usage(const std::string str)
{
    std::cout << "\n\tUsage: " << str << " port[1024+]\n" << std::endl; 
}


int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> server(new TcpServer(port));
    server->InitServer();
    server->StartServer();


    return 0;
}

三、telnet作为客户端进行测试

  1. 其实我们的linux中还有一个工具叫做telnet,这个telnet的底层是使用的是TCP协议,telnet用于在本地通过本地环回地址127.0.0.1然后以及服务端的端口号连接上TCP服务器,可以让telnet作为客户端和我们编写的TCP服务端进行通信
  2. 但是有的读者友友的linux上可能没有安装这个telnet,所以我们使用如下执行安装一下telnet这个工具,然后我们会观察到如下字样complete即为安装telnet工具成功
sudo yum install -y telnet

在这里插入图片描述

  1. 所以此时我们将我们的服务器进行编译,然后运行起来,然后使用telnet进行连接,telent是在本地进行连接的服务器,所以使用本地环回的IP地址127.0.0.1,然后再输入服务器绑定的端口号即可建立连接,如下,左侧服务器已经运行起来了
    在这里插入图片描述
  2. 然后我们在右侧使用telnet作为客户端试着和服务端进行连接,那么启动telnet即输入127.0.0.1 服务器绑定的端口号即可,那么telnet在连接成功之后可以使用ctrl + ]然后再按下回车即可进行输入
    在这里插入图片描述
  3. 所以此时小编在右侧的使用telnet充当的客户端就可以输入数据和左侧的服务端进行通信了,无误
    在这里插入图片描述
  4. 那么如何退出右侧telnet的服务端呢?有很多读者友友心中会想,很简单,无脑ctrl+c呀,多简单,那么下面小编尝试一下无脑ctrl+c是否可以退出右侧的telnet服务端,如下很明显,无脑ctrl+c不可以右侧的telnet服务端
    在这里插入图片描述
  5. 那么正确的退出方式是先按住 ctrl 不松手 然后再按 ] 松开手 此时输入quit即可正常退出telnet客户端
    在这里插入图片描述
  6. 同样的,如下,如果服务端将信道对应的网络文件描述符socket关闭,或者服务端直接终止(服务端终止,代表着网络文件描述符socket也被关闭了),所以这里小编就ctrl+c在左侧直接终止服务器,同样也会导致telnet的退出
    在这里插入图片描述

四、TCP客户端TcpClient.cc

  1. 虽然使用telnet工具充当TCP客户端可以让我们观察到服务端和客户端的通信现象,这代表服务端测试无误,可是我们写客户端了吗?没有,仅仅是借助的telnet工具充当客户端,但是关于客户端的原理以及实现,我们还不清楚,所以下面我们来编写一下TCP客户端TcpClient.cc
  2. 其实TCP客户端的编写套路和UDP客户端的编写套路在最初的时候十分的类似,所以小编这里就基于UDP客户端的基础上进行简要讲解,UDP客户端的实现,详情请点击<——

基本框架

  1. 我们期望用户使用 ./udpclient 124.220.4.187 8080 的方式在命令行运行客户端,所以客户端的main函数同样需要参数
  2. 那么如果进行判断argc的个数即可,如果不等于3,那么说明用户传参错误,所以打印提示,然后终止进程即可
  3. 走到下一步说明此时用户传参正确,那么我们提取服务器的IP地址和端口号port即可
  4. 接下来socket创建套接字即可,如果socket的返回值网络文件描述符sockfd小于0,那么说明创建套接字失败,所以我们打印信息,然后终止进程即可
  5. 当最后使用完成了网络文件描述符sockfd之后,使用close关闭网络文件描述符sockfd即可
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string& str)
{
    std::cout << "\n\tUsage: " << str << " serverip serverport" << std::endl; 
}

// ./tcpclient serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket create err" << std::endl;
        return 1;
    }

    close(sockfd);


    return 0;
}

connect连接

在这里插入图片描述

  1. TCP是面向连接的,所以客户端想要和服务端进行通信,那么客户端需要主动使用connect向服务器发起连接请求
    在这里插入图片描述
  2. connect的使用方法很简单,先传入网络文件描述符,我们看connect的后两个参数和我们之前使用的sendto的后两个参数完全一样,所以对于我们来讲上手使用connect很简单
  3. 所以我们就在本地定义sockaddr_in的结构体对象server,使用memset将各个字段设置为0,然后进行对应字段的初始化即可,需要注意将端口号port和IP地址的主机序列转换成网络字节序列,端口号port的转换使用hton即可,很简单
inet_pton

在这里插入图片描述

  1. 重点是IP地址,这里要将点分十进制的字符串风格的IP地址转换成网络字节序列对应的整数,所以我们使用inet_pton即可,那么依次传入IPv4地址对应的协议家族(域)AF_INET,然后传入要进行转换的点分十进制的字符串风格的IP地址,然后再传入转换后要放入的位置,即server.sin_addr中,那么我们取出server.sin_addr的地址传入即可
  2. 接下来计算出sockaddr_in的结构体对象server的大小len便于进行传参
  3. 所以此时sockaddr_in的结构体对象server的各个字段我们都设置好了,所以接下来就可以进行connect连接了,那么传入网络文件描述符sockfd,然后传入server取地址,接下来强制类型转换为struct sockaddr*,以及传入这个server对象的大小len即可
    在这里插入图片描述
  4. 所以此时我们就发起了一次连接请求,观察connect的返回值,如果连接并且绑定成功,那么就会返回0,如果连接或者绑定失败就会返回一个小于0的数-1,所以这里我们进行判断,如果返回值小于0,那么代表连接或绑定失败,那么我们就打印信息,然后终止进程
  5. 那么问题来了,客户端要不要bind绑定?客户端要不要显示的bind绑定?客户端是什么时候进行绑定的?
    (1)客户端要bind绑定(2)客户端不需要显示的绑定,而是由操作系统进行自动的随机绑定(3)bind绑定是在客户端发起connect请求的时候,操作系统会自动进行端口号的随机bind绑定
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));
socklen_t len = sizeof(server);

//客户端发起connect请求的时候,操作系统会自动进行端口号的随机bind绑定
int n = connect(sockfd, (struct sockaddr*)&server, len);
if(n < 0)
{
    std::cerr << "connect err..." << std::endl;
    return 2;
}

开始进行通信

  1. 那么我们定义一个string类型的字符串,用于接收用户输入,接下来定义一个inbuffer缓冲区用于拷贝服务器发来的在网络文件描述符sockfd对应的套接字对象中缓冲区的数据
  2. 我们期望客户端一旦开始和服务器进行通信,那么就一直进行通信,所以通信的代码逻辑应该基于while死循环的基础上进行编码
  3. 所以首先我们打印消息,提示用户可以开始进行输入,那么使用getline获取一行输入,将输入结果放到message中,所以用户的输入此时就保存在message中
  4. 由于TCP是面向字节流的,并且TPC网络文件描述符fd对应的是一个套接字文件对象,文件同样也是面向字节流的,所以我们就可以使用文件中的读写方式进行通信,即使用read读接收数据,使用write进行写发送数据
  5. 那么作为客户端我们首先要将数据使用write通过网络文件描述符sockfd发送给服务器,并且约定相互之间发送的数据是一个字符串,文件中的字符串不以’\0’结尾,所以我们可以使用message的size接口求出字符串的大小,符合我们需求
  6. 那么我们使用write依次传入网络文件描述符sockfd,字符串,字符串的大小即可
  7. 但是write写入也有可能会失败,那么就会返回一个小于0的数,所以这里我们接收一下返回值,并且进行判断,如果写入失败,那么打印信息,然后break退出循环即可
  8. 下一步说明数据已经成功的发送给了服务器,那么服务器处理完成之后就要将数据发回给客户端,所以我们客户端此时就收到数据了,数据被放在了网络文件描述符sockfd对应的文件描述符对象的读缓冲区中,所以我们使用read进行读取即可
  9. 那么对于read依次传入网络文件描述符sockfd,然后传入接收缓冲区inbuffer,以及缓冲区的大小 - 1,这个-1是为了防止缓冲区被写满导致最后无法在结尾添加’\0’所以要预留出防止’\0’的空间
  10. 使用n接收read的返回值,read返回的是读取的字节数,所以如果read读取成功,那么n应该大于0,所以此时我们将缓冲区的n位置添加上’\0’,然后将字符串进行打印即可
  11. 如果n等于0,那么代表此时服务器对应的写端已经关闭,所以此时我们read读取已经没有意义了,所以此时直接break,如果n小于0,那么代表网络文件描述符不正确,读取失败等,所以此时我们同样break退出
std::string message;
char inbuffer[4096];
while(true)
{
    std::cout << "Please Enter# ";
    std::getline(std::cin, message);

    int n = write(sockfd, message.c_str(), message.size());
    if(n < 0)
    {
        std::cerr << "write err" << std::endl;
        break;
    }

    n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
    if(n > 0)
    {
        inbuffer[n] = 0;
        std::cout << inbuffer << std::endl;
    }
    else
    {
        break;
    }
}

测试

  1. 由于小编使用的是云服务器,并且云服务器默认不开放端口号,所以我们要先开放云服务器的TCP的端口号8080,才可以使用我们的客户端连接服务器,那么如何开放云服务器的特定端口号呢?如下是开放腾讯云服务器的特定端口号的操作步骤
    在这里插入图片描述

  2. 首先,进入腾讯云服务器的控制台,找到登录并点击上图红色框框内的任意位置,进入服务器主界面
    在这里插入图片描述

  3. 接下来点击上方防火墙后,找到下面的添加规则并点击
    在这里插入图片描述
    在这里插入图片描述

  4. 然后应用类型默认自定义即可,来源选择全部IPv4地址,由于本文小编使用的是TCP协议进行的网络通信数据传输,所以协议类型选择TCP协议,端口则输入你想要绑定的端口号即可,最后下方点击确定即可添加成功端口号

  5. 所以下面我们就可以开始进行测试了,那么我们编译服务器和客户端即可,然后依次运行服务器,客户端,无误
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  6. 然后ctrl+c依次退出客户端,退出服务端即可,无误
    在这里插入图片描述
    在这里插入图片描述

五、源代码

makefile

all:tcpserver tcpclient

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

.PHONT:clean
clean:
	rm -f tcpserver tcpclient

TcpServer.hpp

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

const int defaultfd = -1;
const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";
const int backlog = 10;
extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : listensock_(defaultfd), port_(port), ip_(ip)
    {}

    void InitServer()
    {
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            lg(Fatal, "create socket error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);

        //防止偶发性服务器无法立即重启
        int opt = 1;
        setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));

        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &(server.sin_addr));
        socklen_t len = sizeof(server);

        if (bind(listensock_, (struct sockaddr *)&server, len) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind socket success, listensock_: %d", listensock_);

        if (listen(listensock_, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }
        lg(Info, "listen socket success, listensock_: %d", listensock_);
    }

    void StartServer()
    {
        lg(Info, "tcpserver is running...");

        for (;;)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

            lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", 
                sockfd, clientip, clientport);
            
            Service(sockfd, clientip, clientport);
            close(sockfd);
        }
    }

    void Service(int sockfd, const std::string& clientip, const uint16_t& clientport)
    {
        while (true)
        {
            char inbuffer[4096];
            ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << "client say# " << inbuffer << std::endl;

                std::string echo_string = "tcpserver echo# ";
                echo_string += inbuffer;

                write(sockfd, echo_string.c_str(), echo_string.size());                
            }
            else if(n == 0)
            {
                lg(Info, "%s:%d quit, server colse sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else  
            {
                //异常
                lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", 
                    sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }

    ~TcpServer()
    {
        if (listensock_ > 0)
            close(listensock_);
    }

private:
    int listensock_;
    uint16_t port_;
    std::string ip_;
};

TcpClient.cc

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string& str)
{
    std::cout << "\n\tUsage: " << str << " serverip serverport" << std::endl; 
}

// ./tcpclient serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket create err" << std::endl;
        return 1;
    }

    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));
    socklen_t len = sizeof(server);
    
    //客户端发起connect请求的时候,操作系统会自动进行端口号的随机bind绑定
    int n = connect(sockfd, (struct sockaddr*)&server, len);
    if(n < 0)
    {
        std::cerr << "connect err..." << std::endl;
        return 2;
    }

    std::string message;
    char inbuffer[4096];
    while(true)
    {
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        int n = write(sockfd, message.c_str(), message.size());
        if(n < 0)
        {
            std::cerr << "write err" << std::endl;
            break;
        }

        n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        else
        {
            break;
        }
    }

    close(sockfd);


    return 0;
}

Main.cc

#include <iostream>
#include <memory>
#include "TcpServer.hpp"


void Usage(const std::string str)
{
    std::cout << "\n\tUsage: " << str << " port[1024+]\n" << std::endl; 
}


int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> server(new TcpServer(port));
    server->InitServer();
    server->StartServer();


    return 0;
}

Log.hpp

#pragma once

#include <iostream>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdarg>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SIZE 1024

#define Info   0
#define Debug  1
#define Warning 2
#define Error  3
#define Fatal  4

#define Screen 1     //输出到屏幕上
#define Onefile 2    //输出到一个文件中
#define Classfile 3  //根据事件等级输出到不同的文件中

#define LogFile "log.txt" //日志名称


class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }

    void Enable(int method) //改变日志打印方式
    {
        printMethod = method;
    }

    ~Log()
    {}

    std::string levelToString(int level)
    {
        switch(level)
        {
            case Info:
                return "Info";
            case Debug:
                return "Debug";
            case Warning:
                return "Warning";
            case Error:
                return "Error";
            case Fatal:
                return "Fata";
            default:
                return "";
        }
    }

    void operator()(int level, const char* format, ...)
    {
        //默认部分 = 日志等级 + 日志时间
        time_t t = time(nullptr);
        struct tm* ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), 
        ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, 
        ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        char logtxt[2 * SIZE];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        printLog(level, logtxt);
    }

    void printLog(int level, const std::string& logtxt)
    {
        switch(printMethod)
        {
            case Screen:
                std::cout << logtxt << std::endl;
                break;
            case Onefile:
                printOneFile(LogFile, logtxt);
                break;
            case Classfile:
                printClassFile(level, logtxt);
                break;
            default:
                break;
        }
    }

    void printOneFile(const std::string& logname, const std::string& logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if(fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    void printClassFile(int level, const std::string& logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level);

        printOneFile(filename, logtxt);
    }


private:
    int printMethod;
    std::string path;
};

Log lg;


总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐