当前位置:   article > 正文

【Linux系统化学习】网络套接字(编写简单的TCP服务端和客户端)

【Linux系统化学习】网络套接字(编写简单的TCP服务端和客户端)

目录

TCP服务端

创建套接字

解决绑定失败问题

填充网络信息

绑定

设置监听状态

接受请求

收取和反馈消息

完整的服务端封装代码

TCP客户端

创建套接字

填充网络信息

发起连接

发送和收取消息

客户端完整代码

 一些补充


TCP服务端

初始化服务端

创建套接字

和UDP创建套接字的方式差不多,只不过我们要实现的是TCP第二个参数选用:SOCK_STREAM

解决绑定失败问题

服务端关闭绑定失败的问题通常出现在服务端程序退出后,重新启动时,可能会因为之前的套接字仍然处于 TIME_WAIT 状态,导致无法绑定到相同的地址和端口。解决这个问题的方法通常是通过设置 SO_REUSEADDRSO_REUSEPORT 选项。

  1. int opt = 1;
  2. setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

填充网络信息

和UDP填充本地网络信息的方式一样,还是要注意以下几点:

  • 端口号主机序列转网络序列
  • IP地址点分十进制字符串转四字节网络序列
  • 设置任意的IP地址

绑定

和UDP绑定一样,要注意结构体的强转

设置监听状态

因为TCP要建立连接,一般是由客户端发起请求,服务端等待请求的到来。所以服务端要设置监听状态

int listen(int sockfd, int backlog);

返回值

listen() 函数的返回值是一个整数,表示执行结果的状态:

  • 如果成功,返回值为 0。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

参数

  • sockfd:是一个已经通过 socket() 函数创建的套接字描述符,即要进行监听的套接字。

  • backlog:是一个整数,指定在内核中等待处理的连接队列的最大长度。这个参数的具体含义是系统内核在拒绝新连接之前允许处于未连接状态(SYN_RCVD)的连接数量。

    如果队列满了,新的连接会被拒绝,并且客户端可能会收到 ECONNREFUSED 错误。较大的 backlog 值可以容纳更多的等待连接的客户端,但同时也会增加系统资源的消耗。

    如果 backlog 设置为 0,表示不接受连接队列,新连接会立即被拒绝。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

启动服务端

接受请求

accept() 函数用于接受客户端的连接请求,并创建一个新的套接字来处理该连接。这个新的套接字是与客户端建立的连接的专用套接字,通过它可以进行数据的收发。

返回值

accept() 函数的返回值是一个整数,表示新创建的套接字的文件描述符:

  • 如果成功,返回值是新创建的套接字的文件描述符。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

函数调用成功后的返回值是作为我们接下来进行读取数据的文件描述符,旧的套接字依旧作为监听套接字。 

参数

  • sockfd:是一个已经通过 socket() 函数创建并调用 bind() 函数绑定了地址的监听套接字描述符,即待处理连接的监听套接字。

  • addr:是一个指向 struct sockaddr 类型的指针,用于存储客户端的地址信息。这个参数是一个输出参数,accept() 函数会填写客户端的地址信息到这个结构体中。如果不需要知道客户端的地址,可以将这个参数设置为 NULL

  • addrlen:是一个指向 socklen_t 类型的指针,用于指定 addr 结构体的长度。在调用 accept() 函数之前,需要将 addrlen 设置为 addr 结构体的长度。这个参数也是一个输入输出参数,accept() 函数会将实际的客户端地址长度写入到这个变量中。

收取和反馈消息

因为建立连接后得到一个文件描述符,因此我们可以用使用文件的读写方法来进行我们的收取和反馈消息的操作。


完整的服务端封装代码

 TCPserver.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <strings.h>
  6. #include <cerrno>
  7. #include <unistd.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <netinet/in.h>
  11. #include <arpa/inet.h>
  12. using namespace std;
  13. const static int default_backlog = 5;
  14. class TcpServer
  15. {
  16. public:
  17. TcpServer(uint16_t port)
  18. : _port(port), _isrunning(false)
  19. {
  20. }
  21. // 初始化服务器
  22. void Init()
  23. {
  24. // 第一步:套接字创建
  25. // 得到文件描述符
  26. // 本质是文件
  27. // 第一个参数表示域
  28. // 第二个参数表示套接字类型
  29. // 第三个参数为协议 默认缺省
  30. _listensock = socket(AF_INET, SOCK_STREAM, 0);
  31. if (_listensock < 0)
  32. {
  33. cout << "Fatal Error" << endl
  34. << "create socket error, errno code %d ,eror string %d",
  35. errno, strerror(errno);
  36. cout << endl;
  37. exit(2);
  38. }
  39. cout << "create socket success, sockfd : " << _listensock << endl;
  40. int opt=1;
  41. setsockopt(_listensock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
  42. //解决服务端关闭绑定失败问题
  43. // 第二步:填充本地网络信息并且绑定和监听
  44. struct sockaddr_in local;
  45. memset(&local, 0, sizeof(local));
  46. local.sin_family = AF_INET;
  47. local.sin_port = htons(_port);
  48. local.sin_addr.s_addr = INADDR_ANY;
  49. // 设置内核空间 ——绑定
  50. if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) != 0)
  51. {
  52. cout << "bind Error" << "errno code : " << errno << "error string : " << strerror(errno) << endl;
  53. exit(3);
  54. }
  55. cout << "bind success" << endl;
  56. // Tcp需要建立连接,一般是由客户端发起请求 服务端一直等待连接的到来
  57. // Tcp需要监听
  58. // 设置套接字为监听状态
  59. if (listen(_listensock, default_backlog) != 0)
  60. {
  61. cout << "listen Error" << "errno code: " << errno << "error string : " << strerror(errno) << endl;
  62. exit(4);
  63. }
  64. cout << "listen success " << endl;
  65. }
  66. void Sverse(int fd)
  67. {
  68. char buffer[1024];
  69. while (1)
  70. {
  71. // 读取数据
  72. // tcp是面向字节流和文件管道差不多
  73. // 直接使用文件的读写方法即可
  74. ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
  75. if (n > 0)
  76. {
  77. // 对同一个fd进行读写
  78. buffer[n] = 0;
  79. cout << "client say#" << buffer << endl;
  80. string echo_string = "client say#";
  81. echo_string += buffer;
  82. write(fd, echo_string.c_str(), echo_string.size());
  83. }
  84. else if (n == 0) // 如果返回值为0 ,代表读到了文件结尾 (对端关闭了连接)
  85. {
  86. cout << "client quit..." << endl;
  87. break;
  88. }
  89. else
  90. {
  91. cout << "read Error" << "error code : " << errno << "error string : " << strerror(errno) << endl;
  92. break;
  93. }
  94. }
  95. }
  96. void Start()
  97. {
  98. // 服务器启动了
  99. _isrunning = true;
  100. while (1)
  101. {
  102. // 先获取连接
  103. // 后两个参数为输入输出型参数//等同于UDP————recvfrom
  104. // 返回值 成功了返回一个非零的新的文件描述符(网络套接字) 失败了返回-1
  105. //
  106. struct sockaddr_in peer;
  107. socklen_t len = sizeof(peer);
  108. int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
  109. if (sockfd < 0)
  110. {
  111. cout << "accept Error" << "error code: " << errno << "error string: " << strerror(errno) << endl;
  112. // 监听失败继续监听
  113. continue;
  114. }
  115. cout << "accept success , get a new sockfd : " << sockfd << endl;
  116. // 提供服务
  117. Sverse(sockfd);
  118. close(sockfd);
  119. }
  120. }
  121. ~TcpServer()
  122. {
  123. if (_listensock > 0)
  124. {
  125. close(_listensock);
  126. }
  127. }
  128. private:
  129. // 端口号
  130. uint16_t _port;
  131. int _listensock;
  132. bool _isrunning;
  133. };

Main.cc

  1. #include<iostream>
  2. #include<memory>
  3. #include"TcpServer.hpp"
  4. using namespace std;
  5. void Usage(const string & process)
  6. {
  7. cout<<"Usage:"<<endl<<process <<" local_port"<<endl;
  8. }
  9. int main(int argc ,char * argv[])
  10. {
  11. if(argc!=2)
  12. {
  13. Usage(argv[0]);
  14. return 1;
  15. }
  16. uint16_t port = stoi(argv[1]);
  17. auto* tsvr = new TcpServer(port);
  18. tsvr->Init();
  19. tsvr->Start();
  20. delete tsvr;
  21. return 0;
  22. }

TCP客户端

创建套接字

和服务端的一样

填充网络信息

和服务端的一样

发起连接

connect() 函数用于客户端向服务器发起连接请求。它在客户端程序中使用,用于连接到远程服务器。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

返回值

connect() 函数的返回值是一个整数,表示执行结果的状态:

  • 如果成功,返回值为 0。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

 参数

  • sockfd:是一个已经通过 socket() 函数创建的套接字描述符,即待连接的套接字。

  • addr:是一个指向 struct sockaddr 类型的指针,用于存储远程服务器的地址信息。通常是使用 struct sockaddr_instruct sockaddr_in6 结构体来表示 IPv4 或 IPv6 地址。这个参数包含了远程服务器的 IP 地址和端口号。

  • addrlen:是一个 socklen_t 类型的整数,表示 addr 结构体的长度。

注:这个参数调用成功操作系统会自动绑定。 

发送和收取消息

因为socket的本质也是一个文件描述符,因此我们可以像上面一样使用文件的读写方法来发送和收取消息。


客户端完整代码

  1. #include <iostream>
  2. #include <string>
  3. #include <cstring>
  4. #include <unistd.h>
  5. #include <cstdlib>
  6. #include <sys/types.h>
  7. #include <sys/socket.h>
  8. #include <netinet/in.h>
  9. #include <arpa/inet.h>
  10. using namespace std;
  11. bool visitserver(string &serverip, uint16_t serverport, int *cnt)
  12. {
  13. // 创建套接字
  14. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  15. if (sockfd < 0)
  16. {
  17. cout << "client sockfd error" << endl;
  18. return false;
  19. }
  20. bool ret = true;
  21. // 客户端不用绑定 , 客户端必须要有服务端的IP和port , 需要绑定,但是不需要用户显示绑定,因为client系统会随机绑定端口
  22. // Tcp发起连接的时候,client会被操作系统自动绑定
  23. // 发起连接
  24. struct sockaddr_in server;
  25. memset(&server, 0, sizeof(server));
  26. server.sin_family = AF_INET;
  27. server.sin_port = htons(serverport);
  28. // 字符串的点分十进制转为四字节
  29. inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
  30. int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server)); // 自动进行绑定
  31. // 连接失败
  32. if (n < 0)
  33. {
  34. cout << "connect error" << endl;
  35. ret = false;
  36. goto END;
  37. }
  38. // 重连成功,重连次数清零
  39. *cnt = 0;
  40. // 进行通信
  41. while (1)
  42. {
  43. string inbuffer;
  44. cout << "Plase Enter";
  45. getline(cin, inbuffer);
  46. if (inbuffer == "quit")
  47. break;
  48. ssize_t n = write(sockfd, inbuffer.c_str(), inbuffer.size());
  49. if (n > 0)
  50. {
  51. char buffer[1024];
  52. ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);
  53. if (m > 0)
  54. {
  55. buffer[m] = 0;
  56. cout << "get a echo message ->" << buffer << endl;
  57. }
  58. else if (m == 0)
  59. {
  60. break;
  61. }
  62. else
  63. {
  64. ret = false;
  65. goto END;
  66. }
  67. }
  68. else
  69. {
  70. ret = false;
  71. goto END;
  72. }
  73. }
  74. END:
  75. close(sockfd);
  76. return ret;
  77. }
  78. void Usage(const string &process)
  79. {
  80. cout << "Usage : " << process << " server_ip server_port" << endl;
  81. }
  82. int main(int argc, char *argv[])
  83. {
  84. if (argc != 3)
  85. {
  86. Usage(argv[0]);
  87. return 1;
  88. }
  89. string serverip = argv[1];
  90. uint16_t serverport = stoi(argv[2]);
  91. // 定制重连
  92. int cnt = 1;
  93. while (cnt <= 5)
  94. {
  95. bool result = visitserver(serverip, serverport, &cnt);
  96. if (result)
  97. {
  98. cnt = 5;
  99. break;
  100. }
  101. else
  102. {
  103. sleep(1);
  104. cout << "server offline, retrying...,cout : " << cnt++ << endl;
  105. cnt++;
  106. }
  107. }
  108. // 大于重连次数
  109. if (cnt >= 5)
  110. {
  111. cout << "server offline" << endl;
  112. }
  113. return 0;
  114. }
  115. //

一些补充

uint32_t inet_addr(const char *cp);

这个函数在上篇文章中是用来将点分十进制的字符串IP地址转化为四字节的网络序列的,但是这个函数由于它的实现方法原因是不是线程安全的函数,以后我们尽量使用下面的函数。

int inet_pton(int af, const char *src, void *dst);

返回值

  • 如果转换成功,返回值为 1。
  • 如果输入的地址格式不正确,返回值为 0。
  • 如果发生错误,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

参数

  • af:指定地址族(Address Family),通常是 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • src:指向以字符串形式表示的 IP 地址的指针,即点分十进制格式的 IP 地址。
  • dst:指向用于存储结果的缓冲区的指针,通常是一个 struct in_addr 结构体的指针(IPv4)或 struct in6_addr 结构体的指针(IPv6)。

今天对网络套接字的分享到这就结束了,希望大家读完后有很大的收获,也可以在评论区点评文章中的内容和分享自己的看法;个人主页还有很多精彩的内容。您三连的支持就是我前进的动力,感谢大家的支持!!! 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/607216
推荐阅读
相关标签
  

闽ICP备14008679号