TCP网络套接字

[复制链接]
发表于 2026-2-10 14:25:29 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
一、创建套接字

  1. #include <sys/types.h>  
  2. #include <sys/socket.h>  
  3. 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以指示错误缘故原由。
二、绑定

  1. #include <sys/types.h>  
  2. #include <sys/socket.h>  
  3.   
  4. 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以指示错误缘故原由。
  1.     void Init()
  2.     {
  3.         // 创建tcp套接字
  4.         _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
  5.         if (_listensockfd < 0)
  6.         {
  7.             LOG(FATAL, "sockfd create fall!\n");
  8.             exit(SOCKET_ERROR);
  9.         }
  10.         LOG(INFO, "create sockfd success,sockfd:%d\n", _listensockfd);
  11.         // bind
  12.         struct sockaddr_in local;
  13.         local.sin_family = AF_INET;
  14.         local.sin_port = htons(_port);
  15.         local.sin_addr.s_addr = INADDR_ANY;
  16.         int n = ::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
  17.         if (n < 0)
  18.         {
  19.             LOG(FATAL, "bind false!\n");
  20.             exit(BIND_ERROR);
  21.         }
  22.         LOG(INFO, "bind success!\n");
  23.      }
复制代码
三、将套接字设置为监听状态

对于UDP来说绑定完就可以正常举行读取发送数据了,由于UDP协议是无毗连的,但是TCP协议是可毗连的,以是我们必要先将套接字设置为监听状态
  1. int listen(int sockfd, int backlog);
复制代码
参数: 


  • sockfd:这是必要设置为监听状态的套接字的文件形貌符。这个套接字之前应该已经通过 socket函数创建,并通过bind函数绑定到了一个特定的IP所在和端口上。
  • backlog:这个参数界说了内核应该为相应套接字列队的最大毗连数。这个值至少为0,但现实的最大值取决于体系。如果设置为0,则体系会根据其设置来决定一个符合的值。这个值并不是限定同时毗连客户端的数目,而是限定在套接字处于listen状态时,尚未被accept调用的毗连哀求队列的最大长度。
返回值:


  • 乐成时,函数返回0。
  • 失败时,返回-1,并设置错误码。
  1.     void Init()
  2.     {
  3.         // 创建tcp套接字
  4.         _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
  5.         if (_listensockfd < 0)
  6.         {
  7.             LOG(FATAL, "sockfd create fall!\n");
  8.             exit(SOCKET_ERROR);
  9.         }
  10.         LOG(INFO, "create sockfd success,sockfd:%d\n", _listensockfd);
  11.         // bind
  12.         struct sockaddr_in local;
  13.         local.sin_family = AF_INET;
  14.         local.sin_port = htons(_port);
  15.         local.sin_addr.s_addr = INADDR_ANY;
  16.         int n = ::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
  17.         if (n < 0)
  18.         {
  19.             LOG(FATAL, "bind false!\n");
  20.             exit(BIND_ERROR);
  21.         }
  22.         LOG(INFO, "bind success!\n");
  23.         // listen
  24.         n = ::listen(_listensockfd, gbacklog);
  25.         if (n < 0)
  26.         {
  27.             LOG(FATAL, "listen false!\n");
  28.             exit(LISEN_ERROR);
  29.         }
  30.         LOG(INFO, "listen success!\n");
  31.     }
复制代码
四、服务端获取毗连

accept函数是网络编程中用于TCP服务器的一个关键体系调用,它用于从完成毗连队列中取出下一个已完成毗连哀求,并创建一个新的套接字来与该客户端举行通讯。这个函数通常与 listen函数一起使用,在TCP服务器步伐中饰演着吸收客户端毗连的脚色。
  1. #include <sys/socket.h>  
  2.   
  3. 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返回的套接字是我们用来举行数据吸收发送的。
  1.     void Start()
  2.     {
  3.         _isrunning = true;
  4.         while (_isrunning)
  5.         {
  6.             struct sockaddr_in client;
  7.             memset(&client, 0, sizeof(client));
  8.             socklen_t len = sizeof(client);
  9.             int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
  10.             if (sockfd < 0)
  11.             {
  12.                 LOG(ERROR, "accept false!\n");
  13.                 continue;
  14.             }
  15.             LOG(INFO, "connect a link! sockfd:%d\n", sockfd);
  16.             InetAddr addr(client);
  17.             //通过accept返回的套接字我们就可以进行网络通信了
  18.             Service(sockfd, addr);
  19.         }
  20.     }
复制代码
同样我们实现的Service函数就是实现一个简朴的回显函数
  1.     void Service(int sockfd, InetAddr addr)
  2.    {
  3.         while (true)
  4.         {
  5.             char buffer[1024];
  6.             int n = read(sockfd, buffer, sizeof(buffer) - 1);
  7.             if (n > 0)
  8.             {
  9.                 buffer[n] = 0;
  10.                 LOG(DEBUG, "[%s]#%s\n", addr.AddrStr().c_str(), buffer);
  11.                 std::string echo_message = "[udpserver echo]#";
  12.                 echo_message += buffer;
  13.                 n = write(sockfd, echo_message.c_str(), sizeof(echo_message));
  14.                 if (n < 0)
  15.                 {
  16.                     LOG(ERROR, "server write false!\n");
  17.                 }
  18.             }
  19.             else if (n == 0) // 读到文件结尾
  20.             {
  21.                 LOG(INFO, "%s quit!\n", addr.AddrStr().c_str());
  22.                 break;
  23.             }
  24.             else
  25.             {
  26.                 LOG(INFO, "read error!\n");
  27.                 break;
  28.             }
  29.         }
  30.         ::close(sockfd);
  31.     }
复制代码
五、读取发送数据

由于tcp协议是面向字节流的,以是我们可以直接使用read、write向文件形貌符读取发送数据,而为了与UDP协议的函数相类似,OS还提供了recv和send函数
recv
  1. 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
  1. ssize_t send(int sockfd, const void *buf, size_t len, int flags);
复制代码
六、客户端

客户端起首必要创建一个套接字,并将服务端的ip所在端标语等信息录入sockaddr_in布局体中,与UDP套接字的使用一样,客户端不需体现的调用bind函数,其次由于TCP协议是必要毗连的,以是客户端必要先调用connect函数与服务端构建接洽
  1. #include <sys/types.h>  
  2. #include <sys/socket.h>  
  3.   
  4. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
复制代码
  1. //使用方式:./tcpclient ip地址 端口号
  2. int main(int argc, char *argv[])
  3. {
  4.     if (argc != 3)
  5.     {
  6.         std::cerr << "Usage:" << argv[0] << " serverip serverport" << std::endl;
  7.         exit(0);
  8.     }
  9.     std::string serverip = argv[1];
  10.     uint16_t serverport = std::stoi(argv[2]);
  11.     int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  12.     struct sockaddr_in server;
  13.     memset(&server,0,sizeof(server));
  14.     server.sin_family = AF_INET;
  15.     server.sin_port = htons(serverport);
  16.     server.sin_addr.s_addr = inet_addr(serverip.c_str());
  17.     int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
  18.     if (n < 0)
  19.     {
  20.         std::cerr << "connect socket error" << std::endl;
  21.         exit(2);
  22.     }
  23.     while (true)
  24.     {
  25.         std::string sendmessage;
  26.         std::cout << "Please Enter#";
  27.         std::getline(std::cin, sendmessage);
  28.         write(sockfd, sendmessage.c_str(), sizeof(sendmessage));
  29.         char echo_buffer[1024];
  30.         n = read(sockfd, echo_buffer, sizeof(echo_buffer));
  31.         if (n > 0)
  32.         {
  33.             echo_buffer[n] = 0;
  34.             std::cout << echo_buffer << std::endl;
  35.         }
  36.         else
  37.         {
  38.             break;
  39.         }
  40.     }
  41.     ::close(sockfd);
  42.     return 0;
  43. }
复制代码
上述代码实在存在一个标题,由于我们上述的代码是单历程的,而Service业务是一个长服务,他不会自动退出,当第一个客户端进来以后是可以正常实行业务的,但是如果第二个客户端进来以后由于只有一个实行流,第二个客户端无法正常实行业务,只有当第一个客户端实行完退出以后第二个客户端才气正常实行,以是我们继承改进一下
七、多历程版

        我们可以使用fork函数创建出一个子历程,让子历程来实行Service,父历程等候子历程退出后,就可以继承实行循环,吸收别的客户端,但是我们还要思量壅闭等候的标题,我们可以将等候方式设置为非壅闭等候的,但是这种方式有点贫困。我们知道子历程退出后会给父历程发送SIGNALCHLD信号,最简朴的方式就是可以将这个信号设置为忽略,signal(SIGCHLD,SIG_IGN);这里另有另一种方式,我们可以再创建一个孙子历程,并将子历程退出,子历程就直接会被父历程等候,如许的话孙子历程就会酿成孤儿历程,被bash管理,让孙子历程实行我们的业务,如许我们就不必要思量等候的标题了。
        这里另有一个小细节,子历程会继承父历程的文件形貌符表,我们知道每一个客户端会对应一个sockfd,而文件形貌符表本质就是一个数组,也是有数目巨细的,当客户端的数目比力多了的话文件形貌符表大概就会被占满,其次父历程不关心业务实行什么,也就是父历程不关心sockfd,以是每当创建一个子历程发起父历程将sockfd关掉,也发起子历程将listenfd也关掉方式误操纵,如许岂论有多少个客户端,其对应的文件形貌符永久是4
  1.     void Start()
  2.     {
  3.         _isrunning = true;
  4.         while (_isrunning)
  5.         {
  6.             struct sockaddr_in client;
  7.             memset(&client, 0, sizeof(client));
  8.             socklen_t len = sizeof(client);
  9.             int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
  10.             if (sockfd < 0)
  11.             {
  12.                 LOG(ERROR, "accept false!\n");
  13.                 continue;
  14.             }
  15.             LOG(INFO, "connect a link! sockfd:%d\n", sockfd);
  16.             InetAddr addr(client);
  17.             // version1:有缺陷,Service是长服务,由于这个代码是单进程的,第二个客户端进来会执行不了
  18.             // Service(sockfd, addr);
  19.             // version2: 多进程
  20.             pid_t id = fork();
  21.             if (id == 0)
  22.             {
  23.                 // 子进程
  24.                 ::close(_listensockfd); // 防止误操作
  25.                 // 由于是阻塞等待,service不退出父进程就不会向后继续执行循环,简单的方式是signal(SIGCHLD,SIG_IGN);,
  26.                 // 也可以这样写,创建一个孙子进程并直接exit这样子进程就变成了孤儿进程交给bash处理,这样我们就不需要管子进程了
  27.                 if (fork() > 0)
  28.                     exit(0);
  29.                 Service(sockfd, addr);
  30.                 exit(0);
  31.             }
  32.             ::close(sockfd); // 父进程不关心执行什么任务
  33.             pid_t rid = waitpid(id, nullptr, 0);
  34.             if (rid > 0)
  35.             {
  36.                 LOG(INFO, "wait success!\n");
  37.             }
  38.         }
  39.     }
复制代码
八、多线程版

创建一个多线程要求我们在pthread_create时传入一个参数为void*返回值为void*的函数,以是我们可以在类内计划一个静态的Execute函数,而我们想要调用这个函数就必要一个对象,其次我们的业务Service必要传入套接字sockfd和客户端信息Inet_Addr,以是我们可以将这三个元素封装成一个类,将这个类对象作为参数传给Execute
  1.     class ServerData
  2.     {
  3.     public:
  4.         ServerData(int sockfd, InetAddr &addr, TcpServer *td)
  5.             : _sockfd(sockfd), _addr(addr), _td(td)
  6.         {
  7.         }
  8.     public:
  9.         int _sockfd;
  10.         InetAddr _addr;
  11.         TcpServer *_td;
  12.     };
复制代码
  1.     void Start()
  2.     {
  3.         _isrunning = true;
  4.         while (_isrunning)
  5.         {
  6.             struct sockaddr_in client;
  7.             memset(&client, 0, sizeof(client));
  8.             socklen_t len = sizeof(client);
  9.             int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
  10.             if (sockfd < 0)
  11.             {
  12.                 LOG(ERROR, "accept false!\n");
  13.                 continue;
  14.             }
  15.             LOG(INFO, "connect a link! sockfd:%d\n", sockfd);
  16.             InetAddr addr(client);
  17.             // version 3:多线程版
  18.             pthread_t tid;
  19.             ServerData* sd=new ServerData(sockfd,addr,this);
  20.             pthread_create(&tid, nullptr, Excute, sd);
  21.         }
  22.     }
复制代码
九、线程池版

固然这种方式可以,但是很不发起如许写,由于我们写的线程池中的线程也是有限的,而我们的业务是长服务,如许就会导致如果我们的客户端数目大于线程数目时,有些客户端由于没有实行流大概无法获取正常的业务,以是不发起线程池实行长服务
  1.     void Start()
  2.     {
  3.         _isrunning = true;
  4.         while (_isrunning)
  5.         {
  6.             struct sockaddr_in client;
  7.             memset(&client, 0, sizeof(client));
  8.             socklen_t len = sizeof(client);
  9.             int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
  10.             if (sockfd < 0)
  11.             {
  12.                 LOG(ERROR, "accept false!\n");
  13.                 continue;
  14.             }
  15.             LOG(INFO, "connect a link! sockfd:%d\n", sockfd);
  16.             InetAddr addr(client);
  17.             // version4:线程池
  18.             // ps:线程池不适合做长服务,因为线程池的线程数量也是有限的,如果客户端数量超过线程数量,再有客户端加进来也得不到服务
  19.             task_t t = std::bind(&TcpServer::Service, this, sockfd, addr);
  20.             ThreadPool<task_t>::GetInstance()->Enqueue(t);
  21.         }
  22.     }
复制代码
上述全部代码可以参考:
张得帅c/Linux

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表