TCP网络套接字

一、创建套接字
#include <sys/types.h>  
#include <sys/socket.h>  
 
int socket(int domain, int type, int protocol);

参数:

  • **domain:**指定使用的协议族。常见的取值有AF_INET(IPv4)和AF_INET6(IPv6)。这个参数决定了地址的格式和类型。
  • **type:**指定套接字的类型。常见的取值有SOCK_STREAM(流套接字,提供有序的、可靠的、双向的和基于连接的字节流,通常使用TCP协议)和SOCK_DGRAM(数据报套接字,支持无连接的、不可靠的和使用固定大小缓冲区的数据报服务,通常使用UDP协议)。
  • **protocol:**指定协议编号。通常可以设置为0,让系统根据domain和type自动选择合适的协议。

返回值:

  • 成功:socket函数成功执行时,返回一个非负整数,续的socket编程中用于标识和操作该套接字即套接字的文件描述符。这个描述符在后。
  • 失败:如果socket函数调用失败,将返回-1,并设置相应的errno以指示错误原因。
二、绑定
#include <sys/types.h>  
#include <sys/socket.h>  
  
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind函数的作用是将一个套接字与一个具体的地址(包括IP地址和端口号)绑定。这样,当套接字进行通信时,就可以使用这个指定的地址作为通信的源地址。

参数:

  • sockfd:标识一个已经创建但尚未绑定的套接字的文件描述符。
  • addr:指向一个包含地址信息的结构体的指针。对于IPv4,通常使用struct sockaddr_in;对于IPv6,则使用struct sockaddr_in6。
  • addrlen:addr结构体的大小,通常可以使用sizeof操作符获取。

返回值:

  • 成功时,bind函数返回0。

  • 失败时,返回-1,并设置相应的errno以指示错误原因。

      void Init()
      {
          // 创建tcp套接字
          _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
          if (_listensockfd < 0)
          {
              LOG(FATAL, "sockfd create fall!\n");
              exit(SOCKET_ERROR);
          }
          LOG(INFO, "create sockfd success,sockfd:%d\n", _listensockfd);
          // bind
          struct sockaddr_in local;
          local.sin_family = AF_INET;
          local.sin_port = htons(_port);
          local.sin_addr.s_addr = INADDR_ANY;
          int n = ::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
          if (n < 0)
          {
              LOG(FATAL, "bind false!\n");
              exit(BIND_ERROR);
          }
          LOG(INFO, "bind success!\n");
       }
    
三、将套接字设置为监听状态

对于UDP来说绑定完就可以正常进行读取发送数据了,因为UDP协议是无连接的,但是TCP协议是可连接的,所以我们需要先将套接字设置为监听状态

int listen(int sockfd, int backlog);

参数:

  • **sockfd:**这是需要设置为监听状态的套接字的文件描述符。这个套接字之前应该已经通过 socket函数创建,并通过bind函数绑定到了一个特定的IP地址和端口上。
  • **backlog:**这个参数定义了内核应该为相应套接字排队的最大连接数。这个值至少为0,但实际的最大值取决于系统。如果设置为0,则系统会根据其配置来决定一个合适的值。这个值并不是限制同时连接客户端的数量,而是限制在套接字处于listen状态时,尚未被accept调用的连接请求队列的最大长度。

返回值:

  • 成功时,函数返回0。

  • 失败时,返回-1,并设置错误码。

      void Init()
      {
          // 创建tcp套接字
          _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
          if (_listensockfd < 0)
          {
              LOG(FATAL, "sockfd create fall!\n");
              exit(SOCKET_ERROR);
          }
          LOG(INFO, "create sockfd success,sockfd:%d\n", _listensockfd);
          // bind
          struct sockaddr_in local;
          local.sin_family = AF_INET;
          local.sin_port = htons(_port);
          local.sin_addr.s_addr = INADDR_ANY;
          int n = ::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
          if (n < 0)
          {
              LOG(FATAL, "bind false!\n");
              exit(BIND_ERROR);
          }
          LOG(INFO, "bind success!\n");
          // listen
          n = ::listen(_listensockfd, gbacklog);
          if (n < 0)
          {
              LOG(FATAL, "listen false!\n");
              exit(LISEN_ERROR);
          }
          LOG(INFO, "listen success!\n");
      }
    
四、服务端获取连接

accept函数是网络编程中用于TCP服务器的一个关键系统调用,它用于从完成连接队列中取出下一个已完成连接请求,并创建一个新的套接字来与该客户端进行通信。这个函数通常与 listen函数一起使用,在TCP服务器程序中扮演着接收客户端连接的角色。

#include <sys/socket.h>  
  
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • sockfd:这是之前通过 socket函数创建的,并且已经通过bind和listen函数准备就绪以接受连接的监听套接字的文件描述符。
  • addr这是一个指向 sockaddr结构的指针,该结构将被填充以表示连接客户端的地址信息。如果调用者对客户端的地址不感兴趣,可以将此参数设置为NULL。
  • addrlen:这是一个指向socklen_t变量的指针,该变量在调用前应该包含 addr缓冲区的大小,在调用后,它将包含实际存储在addr中的地址信息的实际字节数。

返回值:

  • 成功时,返回一个新的套接字文件描述符,该描述符用于与连接的客户端进行通信。
  • 失败时,返回-1,并设置错误码。

注意:到目前为止一共出现了两个套接字,一个是我们用socket函数创建的,另一个是accept返回的,其中我们创建的套接字是用来监听的,所以我们才把他命名为listensockfd,而accept返回的套接字是我们用来进行数据接收发送的。

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in client;
            memset(&client, 0, sizeof(client));

            socklen_t len = sizeof(client);
            int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(ERROR, "accept false!\n");
                continue;
            }
            LOG(INFO, "connect a link! sockfd:%d\n", sockfd);
            InetAddr addr(client);
            //通过accept返回的套接字我们就可以进行网络通信了
            Service(sockfd, addr);

        }
    }

同样我们实现的Service函数就是实现一个简单的回显函数

    void Service(int sockfd, InetAddr addr)
   {
        while (true)
        {
            char buffer[1024];
            int n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                LOG(DEBUG, "[%s]#%s\n", addr.AddrStr().c_str(), buffer);
                std::string echo_message = "[udpserver echo]#";
                echo_message += buffer;
                n = write(sockfd, echo_message.c_str(), sizeof(echo_message));
                if (n < 0)
                {
                    LOG(ERROR, "server write false!\n");
                }
            }
            else if (n == 0) // 读到文件结尾
            {
                LOG(INFO, "%s quit!\n", addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(INFO, "read error!\n");
                break;
            }
        }
        ::close(sockfd);
    }
五、读取发送数据

由于tcp协议是面向字节流的,所以我们可以直接使用read、write向文件描述符读取发送数据,而为了与UDP协议的函数相类似,OS还提供了recv和send函数

recv

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:

  • sockfd/s:socket文件描述符或套接字描述符,它指定了要从中读取数据的socket。
  • buf:指向缓冲区的指针,用于存储接收到的数据。这个缓冲区应该足够大,以存储预期的数据量。
  • len:指定了buf缓冲区的大小,即函数最多可以读取的字节数。
  • flags:用于指定接收操作的行为,这个参数通常是0,表示阻塞读取

返回值:

  • 如果成功,recv返回实际读取的字节数,该值可能小于请求读取的字节数(例如,如果数据不足或对方关闭了连接)。
  • 如果连接被对方正常关闭,并且已经读取了所有可用的数据,recv将返回0。
  • 如果出现错误,recv 将返回-1

send

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
六、客户端

客户端首先需要创建一个套接字,并将服务端的ip地址端口号等信息录入sockaddr_in结构体中,与UDP套接字的使用一样,客户端不需显示的调用bind函数,其次由于TCP协议是需要连接的,所以客户端需要先调用connect函数与服务端构建联系

#include <sys/types.h>  
#include <sys/socket.h>  
  
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//使用方式:./tcpclient ip地址 端口号
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage:" << argv[0] << " serverip serverport" << std::endl;
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

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

    while (true)
    {
        std::string sendmessage;
        std::cout << "Please Enter#";
        std::getline(std::cin, sendmessage);

        write(sockfd, sendmessage.c_str(), sizeof(sendmessage));

        char echo_buffer[1024];
        n = read(sockfd, echo_buffer, sizeof(echo_buffer));
        if (n > 0)
        {
            echo_buffer[n] = 0;
            std::cout << echo_buffer << std::endl;
        }
        else
        {
            break;
        }
    }
    ::close(sockfd);
    return 0;
}

上述代码其实存在一个问题,由于我们上述的代码是单进程的,而Service业务是一个长服务,他不会主动退出,当第一个客户端进来以后是可以正常执行业务的,但是如果第二个客户端进来以后由于只有一个执行流,第二个客户端无法正常执行业务,只有当第一个客户端执行完退出以后第二个客户端才能正常执行,所以我们继续改进一下

七、多进程版

我们可以利用fork函数创建出一个子进程,让子进程来执行Service,父进程等待子进程退出后,就可以继续执行循环,接收别的客户端,但是我们还要考虑阻塞等待的问题,我们可以将等待方式设置为非阻塞等待的,但是这种方式有点麻烦。我们知道子进程退出后会给父进程发送SIGNALCHLD信号,最简单的方式就是可以将这个信号设置为忽略,signal(SIGCHLD,SIG_IGN);这里还有另一种方式,我们可以再创建一个孙子进程,并将子进程退出,子进程就直接会被父进程等待,这样的话孙子进程就会变成孤儿进程,被bash管理,让孙子进程执行我们的业务,这样我们就不需要考虑等待的问题了。

这里还有一个小细节,子进程会继承父进程的文件描述符表,我们知道每一个客户端会对应一个sockfd,而文件描述符表本质就是一个数组,也是有数量大小的,当客户端的数量比较多了的话文件描述符表可能就会被占满,其次父进程不关心业务执行什么,也就是父进程不关心sockfd,所以每当创建一个子进程建议父进程将sockfd关掉,也建议子进程将listenfd也关掉方式误操作,这样不论有多少个客户端,其对应的文件描述符永远是4

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in client;
            memset(&client, 0, sizeof(client));

            socklen_t len = sizeof(client);
            int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(ERROR, "accept false!\n");
                continue;
            }
            LOG(INFO, "connect a link! sockfd:%d\n", sockfd);
            InetAddr addr(client);
            // version1:有缺陷,Service是长服务,由于这个代码是单进程的,第二个客户端进来会执行不了
            // Service(sockfd, addr);

            // version2: 多进程
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程
                ::close(_listensockfd); // 防止误操作
                // 由于是阻塞等待,service不退出父进程就不会向后继续执行循环,简单的方式是signal(SIGCHLD,SIG_IGN);,
                // 也可以这样写,创建一个孙子进程并直接exit这样子进程就变成了孤儿进程交给bash处理,这样我们就不需要管子进程了
                if (fork() > 0)
                    exit(0);
                Service(sockfd, addr);
                exit(0);
            }
            ::close(sockfd); // 父进程不关心执行什么任务
            pid_t rid = waitpid(id, nullptr, 0);
            if (rid > 0)
            {
                LOG(INFO, "wait success!\n");
            }

        }
    }
八、多线程版

创建一个多线程要求我们在pthread_create时传入一个参数为void*返回值为void*的函数,所以我们可以在类内设计一个静态的Execute函数,而我们想要调用这个函数就需要一个对象,其次我们的业务Service需要传入套接字sockfd和客户端信息Inet_Addr,所以我们可以将这三个元素封装成一个类,将这个类对象作为参数传给Execute

    class ServerData
    {
    public:
        ServerData(int sockfd, InetAddr &addr, TcpServer *td)
            : _sockfd(sockfd), _addr(addr), _td(td)
        {
        }

    public:
        int _sockfd;
        InetAddr _addr;
        TcpServer *_td;
    };


    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in client;
            memset(&client, 0, sizeof(client));

            socklen_t len = sizeof(client);
            int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(ERROR, "accept false!\n");
                continue;
            }
            LOG(INFO, "connect a link! sockfd:%d\n", sockfd);
            InetAddr addr(client);
            // version 3:多线程版
            pthread_t tid;
            ServerData* sd=new ServerData(sockfd,addr,this);
            pthread_create(&tid, nullptr, Excute, sd);
        }
    }
九、线程池版

虽然这种方式可以,但是很不建议这样写,因为我们写的线程池中的线程也是有限的,而我们的业务是长服务,这样就会导致如果我们的客户端数量大于线程数量时,有些客户端因为没有执行流可能无法获取正常的业务,所以不建议线程池执行长服务

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in client;
            memset(&client, 0, sizeof(client));

            socklen_t len = sizeof(client);
            int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(ERROR, "accept false!\n");
                continue;
            }
            LOG(INFO, "connect a link! sockfd:%d\n", sockfd);
            InetAddr addr(client);
            // version4:线程池
            // ps:线程池不适合做长服务,因为线程池的线程数量也是有限的,如果客户端数量超过线程数量,再有客户端加进来也得不到服务
            task_t t = std::bind(&TcpServer::Service, this, sockfd, addr);
            ThreadPool<task_t>::GetInstance()->Enqueue(t);
        }
    }

上述所有代码可以参考:

张得帅c/Linux

相关推荐
久绊A2 小时前
网络信息系统的整个生命周期
网络
_PowerShell2 小时前
[ DOS 命令基础 3 ] DOS 命令详解-文件操作相关命令
网络·dos命令入门到精通·dos命令基础·dos命令之文件操作命令详解·文件复制命令详解·文件对比命令详解·文件删除命令详解·文件查找命令详解
_.Switch5 小时前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
qq_254674415 小时前
工作流初始错误 泛微提交流程提示_泛微协同办公平台E-cology8.0版本后台维护手册(11)–系统参数设置
网络
JokerSZ.5 小时前
【基于LSM的ELF文件安全模块设计】参考
运维·网络·安全
小松学前端7 小时前
第六章 7.0 LinkList
java·开发语言·网络
城南vision8 小时前
计算机网络——TCP篇
网络·tcp/ip·计算机网络
Ciderw8 小时前
块存储、文件存储和对象存储详细介绍
网络·数据库·nvme·对象存储·存储·块存储·文件存储
Tony聊跨境9 小时前
独立站SEO类型及优化:来检查这些方面你有没有落下
网络·人工智能·tcp/ip·ip