【Linux】网络编程——TcpServer

一、Tcp Socket 常见的API介绍

       相比于之前所学习的UDP,TCP是较为麻烦一下的,因为TCP协议比UDP协议要难一点,而且TCP需要先建立一条连接才能继续通信,而UDP是直接进行通信。下面来看一看,TCP要使用的API函数接口,顺便复习一下:

1.1 socket函数

函数的原型:

函数的功能:

   socket 函数的主要功能是创建一个套接字,用于网络通信。创建的套接字可以用于各种网络操作,如连接到远程服务器、接收和发送数据等。成功创建的套接字用于建立和管理网络连接或进行数据传输。

函数的参数:

  1. domain: 套接字的协议族。常见的值有:

    • AF_INET:用于 IPv4 地址。
    • AF_INET6:用于 IPv6 地址。
    • AF_UNIX 或 AF_LOCAL:用于本地进程间通信(IPC)。
  2. type: 套接字的类型,指定了套接字的传输方式。常见的值有:

    • SOCK_STREAM:面向连接的套接字,使用 TCP 协议,提供可靠的数据传输。
    • SOCK_DGRAM:无连接的套接字,使用 UDP 协议,提供不可靠的数据传输。
    • SOCK_RAW:原始套接字,通常用于访问更底层的协议。
  3. protocol: 套接字所使用的协议。对于大多数应用,设置为 0 表示自动选择合适的协议(通常是与 type 相关的默认协议)。如果需要指定具体协议,可以用如下值:

    • IPPROTO_TCP:与 SOCK_STREAM 配合使用,表示使用 TCP 协议。
    • IPPROTO_UDP:与 SOCK_DGRAM 配合使用,表示使用 UDP 协议。

函数的返回值:

  • 成功: 返回一个非负的整型文件描述符,表示创建的套接字。
  • 失败: 返回 -1,并且设置 errno 以指示错误原因。常见的错误包括 ENOMEM(内存不足)、EAFNOSUPPORT(地址族不支持)、EPROTONOSUPPORT(协议不支持)等。

1.2 Bind函数

函数的原型:

函数的功能:

  bind 函数将指定的地址(如 IP 地址和端口号)分配给一个已创建的套接字。对于服务器应用,这通常是设置套接字监听特定端口的步骤。

函数的参数:

  1. sockfd: 套接字文件描述符,由 socket 函数返回。
  2. addr: 指向 struct sockaddr 结构体的指针,包含要绑定的地址信息。
  3. addrlenaddr 参数所指向的地址结构体的大小,以字节为单位。

函数的返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno 以指示错误原因,如 EADDRINUSE(地址已在使用中)、EADDRNOTAVAIL(地址不可用)等。

1.3 listen函数

函数的原型:

函数的功能:

  listen 函数的主要功能是将套接字设置为被动模式,以便它可以接收传入的连接请求。调用 listen 后,套接字会进入“监听”状态,等待客户端发起连接请求。

函数的参数:

  • sockfd: 套接字描述符。这个描述符是之前通过 socket 函数创建的,并且在调用 bind 函数后用来标识套接字。

  • backlog: 指定在套接字上允许的最大未完成连接队列的长度。这个队列用于存放尚未被 accept 函数接受的连接请求。当连接请求的数量超过这个值时,新连接请求可能会被拒绝或忽略。

函数的返回值:

  • 成功: 如果成功,返回 0
  • 失败: 如果调用失败,返回 -1,并设置 errno 以指示错误原因。常见的错误代码包括 EBADF(无效的套接字描述符)、ENOTSOCK(描述符不是一个套接字)、EADDRINUSE(地址已被使用)等。

1.4 accept函数

函数的原型:

函数的功能:

   accept 函数的主要功能是从已监听的套接字中接受一个传入的连接请求,创建一个新的套接字用于与客户端进行通信。调用 accept 函数后,服务器可以通过返回的新套接字与客户端交换数据。

函数的参数:

  • sockfd: 已监听的套接字描述符,即之前通过 socket 函数创建并通过 bindlisten 函数设置好的套接字。

  • addr: 指向 sockaddr 结构体的指针,用于存储客户端的地址信息。如果不需要地址信息,可以传入 NULL

  • addrlen: 指向 socklen_t 类型的变量的指针,该变量表示 addr 参数所指向的地址结构体的大小。accept 函数会更新这个变量以反映实际的地址长度。如果 addrNULL,这个参数可以被忽略。

函数的返回值:

  • 成功: 返回一个新的套接字描述符,这个描述符用于与客户端进行通信。新套接字是与连接相关的独立套接字,且继承了原套接字的属性。

  • 失败: 返回 -1,并设置 errno 以指示错误原因。常见的错误代码包括 EBADF(无效的套接字描述符)、EINTR(调用被中断)、ENOTSOCK(描述符不是一个套接字)、EOPNOTSUPP(套接字类型不支持)等。

1.5 connect函数

函数的原型:

函数的功能:

  connect 函数用于在客户端程序中与远程服务器建立连接。当客户端想要与服务器通信时,它使用 connect 函数来请求连接。成功调用 connect 后,客户端与服务器之间就可以进行数据传输了。

函数的参数:

  • sockfd: 这是一个套接字文件描述符,它是通过 socket 函数创建的。这个套接字用于建立连接。

  • addr: 这是一个指向 struct sockaddr 结构体的指针,它包含了要连接的远程主机的地址信息。这个结构体的具体类型通常取决于协议族,例如 struct sockaddr_in 用于 IPv4。

  • addrlen: 这是 addr 指向的地址结构的大小,以字节为单位。通常,可以使用 sizeof(struct sockaddr_in) 来获得这个大小。

函数的返回值:

  • 成功: 返回 0 表示连接成功。
  • 失败: 返回 -1,并且设置 errno 以指示错误原因。例如,errno 可能会被设置为 ECONNREFUSED 表示目标主机拒绝连接,或 ETIMEDOUT 表示连接超时等。

二、TcpServer代码的编写

 我们先来看一看Tcp服务器编写的步骤:

  1. 创建TCP监听套接字
  2. 将TCP服务器的详细地址与TCP监听套接字进行绑定
  3. TCP服务器开启监听
  4. TCP服务器利用accept函数创建出新的套接字,通过新的套接字与客户端进行通信

2.1 TcpServer的初始化函数

    void InitServer()
    {
        // 1. 创建套接字
        _listensock = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            // 创建失败
            LOG(FATAL, "sockfd create failed!\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "sockfd create success, sockfd is %d\n", _listensock);

        // 2. 绑定套接字
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        int n = ::bind(_listensock, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            // 绑定失败
            LOG(FATAL, "bind sockfd failed!\n");
            exit(BIND_ERROR);
        }

        // 3. tcp是面向连接的,所以通信之前,需要先建立连接
        // tcpserver启动,未来首先要一定等待用户的到来,需要进行监听
        n = ::listen(_listensock, backlog);
        if (n < 0)
        {
            // 监听失败
            LOG(FATAL, "listen sockfd failed!\n");
            exit(LISTEN_ERROR);
        }
    }

2.2 TcpServer的启动函数

void Loop()
    {
        // 4. 不能直接收数据,先获取连接
        // accept在通信之前先获取客户端的地址, 返回值是文件描述符
        // 这个文件描述符是什么?? 每建立一个链接就会有一个套接字, 用于IO操作
        _isrunning = true;
        while (_isrunning)
        {
            // 利用accept函数进行创建出新的套接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "sockfd create failed!\n");
                continue;
            }

            // // version 0
            // Service(sockfd, InetAddr(peer)); // 一次只能处理一个请求

            // // version 1 : 采用多进程
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     // 子进程
            //     // 子进程与父进程的文件描述表是独立的,子进程从父进程赋值一份,只关心sockfd
            //     ::close(_listensock); // 建议关掉
            //     if (fork() > 0)
            //         exit(0);
            //     Service(sockfd, InetAddr(peer)); // 孤儿进程
            //     exit(0);
            // }

            // // 父进程将新获取的文件描述交给子进程,只关心listensock
            // ::close(sockfd);
            // waitpid(id, nullptr, 0);

            // // version 2 : 采用多线程
            // pthread_t t;
            // ThreadDate *td = new ThreadDate(sockfd, InetAddr(peer), this);
            // pthread_create(&t, nullptr, handlerSock, td);

            // // version 3 : 线程池技术
            // test_t t = std::bind(&TcpServer::Service, this, sockfd, InetAddr(peer));
            // ThreadPool<test_t>::GetInstance()->Enqueue(t);
        }
        _isrunning = false;
    }

2.3 不同版本的Tcp服务器处理

    void Service(int sockfd, InetAddr addr)
    {
        LOG(INFO, "tcpserver success...\n");
        while (true)
        {
            char buffer[1024];
            int n = ::read(sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << buffer << std::endl;

                std::string echo_string = "[server echo]";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                // client关闭链接
                LOG(DEBUG, "%s quit\n", addr.Ip());
                break;
            }
            else
            {
                LOG(FATAL, "service failed!\n");
                break;
            }
        }
        ::close(sockfd);
    }

2.3.1 版本1——单线程版本

       在这个版本中,我们只需要服务器进行读取客户端发来的数据,然后将读取到的结果返回给客户端。

Service(sockfd, InetAddr(peer)); // 一次只能处理一个请求

2.3.2 版本2——采用多进程版本

       在这个版本中,我们需要利用fork函数创建出子进程,让子进程去处理业务,但是这样,父进程需要等待回收子进程,这样会进行阻塞;所以在这里,我们需要在子进程中再次调用fork函数创建出子进程,并将父进程进行退出,使得子进程变成孤儿进程,交给bash管理,最后将服务处理交给“孙子进程”。

            // version 1 : 采用多进程
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程
                // 子进程与父进程的文件描述表是独立的,子进程从父进程赋值一份,只关心sockfd
                ::close(_listensock); // 建议关掉
                if (fork() > 0)
                    exit(0);
                Service(sockfd, InetAddr(peer)); // 孤儿进程
                exit(0);
            }

            // 父进程将新获取的文件描述交给子进程,只关心listensock
            ::close(sockfd);
            waitpid(id, nullptr, 0);

2.3.3 版本3——采用多线程

       在这个版本中,我们所使用的是自己定义的线程函数,我们需要定义出ThreadDate结构体,使用线程创建函数创建出新线程,将任务处理交给新线程,并将新线程分离。

// ThreadDate结构体
class ThreadDate
{
public:
    ThreadDate(int sockfd, InetAddr addr, TcpServer *s) : _sockfd(sockfd), _addr(addr), self(s) {}

    InetAddr _addr;
    int _sockfd;
    TcpServer *self;
};
static void *HandlerSock(void *args)
{
    pthread_detach(pthread_self());
    ThreadDate *td = static_cast<ThreadDate *>(args);
    td->self->_service(td->_sockfd, td->_addr);
    delete td;
    return nullptr;
}

// version 2 : 采用多线程
pthread_t t;
ThreadDate *td = new ThreadDate(sockfd, InetAddr(peer), this);
pthread_create(&t, nullptr, handlerSock, td);

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加油,旭杏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值