当前位置:   article > 正文

简单的TCP网络程序·单进程(后端服务器)_tcp服务端程序

tcp服务端程序

目录

文件1:tcpServer.cc

文件2:tcpServer.hpp

1.提出日志概念 -- 在后续完善

日志格式 -- 暂定简单的打印功能

2.创建套接字

SOCK_STREAM -- socket参数

3.bind自己的套接字

4.设置socket 为监听状态 *

新接口1:listen

函数1:initServer()

新接口2:accept *

接口1:read

接口2:write

文件描述符本质是数组下标 -- 有限

ulimit -- 查看本机可以开多少个文件描述符

函数2:serviceIO()

至此基本的功能完成 -- 测试1

准备工作

文件3:tcpClient.cc

文件4:tcpClient.hpp

函数3:initClient()

新接口3:connect *

函数4:start()

至此TCP通信的功能完成 -- 测试2

全部代码

log.hpp

makefile

tcpClient.cc

tcpClient.hpp

tcpServer.cc

tcpServer.hpp


直接代码开整

文件1:tcpServer.cc

准备阶段 -- 目前和UDP是一样的

  1. #include "tcpServer.hpp"
  2. #include <memory>
  3. using namespace std;
  4. using namespace server;
  5. static void Usage(string proc)
  6. {
  7. cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
  8. }
  9. // tcp服务器,启动和 udp server 一模一样
  10. // ./tcpserver lock_port
  11. int main(int argc, char *argv[])
  12. {
  13. if (argc != 2)
  14. {
  15. Usage(argv[0]);
  16. exit(USAGE_ERR);
  17. }
  18. uint16_t port = atoi(argv[1]);
  19. unique_ptr<TcpServer> tsvr(new TcpServer());
  20. tsvr->initServer();
  21. tsvr->start();
  22. return 0;
  23. }

文件2:tcpServer.hpp

1.提出日志概念 -- 在后续完善

日志格式 -- 暂定简单的打印功能

这个日志在后序完善TCP之后再进行修改,现在只实现简单的打印功能

2.创建套接字

SOCK_STREAM -- socket参数

3.bind自己的套接字

        这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到

4.设置socket 为监听状态 *

新接口1:listen

底层链接的长度+1,先不管他,在后序讲原理再讲述

函数1:initServer()

  1. void initServer()
  2. {
  3. // 1. 创建socket文件套接字对象 -- 流式套接字
  4. _sock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0
  5. if (_sock < 0)
  6. {
  7. logMessage(FATAL, "create socket error");
  8. exit(SOCKET_ERR);
  9. }
  10. logMessage(NORMAL, "create socket success");
  11. // 2.bind绑定自己的网路信息 -- 注意包含头文件
  12. struct sockaddr_in local;
  13. memset(&local, 0, sizeof(local));
  14. local.sin_family = AF_INET;
  15. local.sin_port = htons(_port); // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到
  16. local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
  17. if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
  18. {
  19. logMessage(FATAL, "bind socket error");
  20. exit(BIND_ERR);
  21. }
  22. logMessage(NORMAL, "bind socket success");
  23. // 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程
  24. if (listen(_sock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑
  25. {
  26. logMessage(FATAL, "listen socket error");
  27. exit(LISTEN_ERR);
  28. }
  29. logMessage(FATAL, "listen socket success");
  30. }

注意这里是起始版本,在认识下面的一个接口的时候,需要整改

新接口2:accept *

        一个比喻:就像一家饭店的门口招呼人的张三,当张三从外边招呼人进来的时候,就向饭店里面喊人,让李四去服务客人,但是张三不会进来,又返回去在门口拉客

        因为随着客户端的不断增多,TCP服务器上可能存在多个套接字,就像饭店里面会有多个客人有多个服务员

至此我们需要把之前的_sock 修改为 _listensock

至此我们获取的sock就是一个文件操作符,可以使用文件操作类的函数进行处理,例如read之类的

接口1:read

从一个文件描述符中读取

接口2:write

文件描述符本质是数组下标 -- 有限

ulimit -- 查看本机可以开多少个文件描述符

函数2:serviceIO()

  1. void serviceIO(int sock)
  2. {
  3. // 先用最简单的,读取再写回去
  4. char buffer[1024];
  5. while (true)
  6. {
  7. ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
  8. if (n > 0)
  9. {
  10. // 截至目前,我们把读到的数据当作字符串
  11. buffer[n] = 0;
  12. std::cout << "recv message: " << buffer << std::endl;
  13. std::string outbuffer = buffer;
  14. outbuffer += "server[echo]";
  15. write(sock, outbuffer.c_str(), outbuffer.size()); // 在多路转接的时候再详谈write的返回值
  16. }
  17. else if(n == 0)
  18. {
  19. // 代表client退出 -- 把它想象成一个建立好的管道,客户端不写了,并且把它的文件描述符关了,读端就会像管道一样读到 0 TCP同理
  20. logMessage(NORMAL, "client quit, me too!");
  21. }
  22. }
  23. }

至此基本的功能完成 -- 测试1

准备工作

文件3:tcpClient.cc

调用逻辑 

  1. #include "tcpClient.hpp"
  2. #include <memory>
  3. using namespace std;
  4. static void Usage(string proc)
  5. {
  6. cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
  7. }
  8. // ./tcpclient serverip serverport 调用逻辑
  9. int main(int argc, char *argv[])
  10. {
  11. if(argc != 3)
  12. {
  13. Usage(argv[0]);
  14. exit(1);
  15. }
  16. string serverip = argv[1];
  17. uint16_t serverport = atoi(argv[2]);
  18. unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));
  19. tcli->initClient();
  20. tcli->start();
  21. return 0;
  22. }

文件4:tcpClient.hpp

函数3:initClient()

  1. void initClient()
  2. {
  3. // 1. 创建socket
  4. _sock = socket(AF_INET, SOCK_STREAM, 0);
  5. if (_sock < 0)
  6. {
  7. // 客户端也可以有日志,不过这里就不再实现了,直接打印错误
  8. std::cout << "socket create error" << std::endl;
  9. exit(2);
  10. }
  11. // 2. tcp的客户端要不要bind? 要的! 但是不需要显示bind,这里的client port要让OS自定!
  12. // 3. 要不要listen? -- 不需要!客户端不需要建立链接
  13. // 4. 要不要accept? -- 不要!
  14. // 5. 要什么? 要发起链接!
  15. }

新接口3:connect *

函数4:start()

  1. void start()
  2. {
  3. struct sockaddr_in server;
  4. memset(&server, 0, sizeof(server));
  5. server.sin_family = AF_INET;
  6. server.sin_port = htons(_serverport);
  7. server.sin_addr.s_addr = inet_addr(_serverip.c_str());
  8. if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
  9. {
  10. std::cerr << "socket connect error" << std::endl;
  11. }
  12. else
  13. {
  14. std::string msg;
  15. while (true)
  16. {
  17. std::cout << "Enter# ";
  18. std::getline(std::cin, msg);
  19. write(_sock, msg.c_str(), msg.size());
  20. char buffer[NUM];
  21. int n = read(_sock, buffer, sizeof(buffer) - 1);
  22. if (n > 0)
  23. {
  24. // 目前我们把读到的数据当成字符串, 截至目前
  25. buffer[n] = 0;
  26. std::cout << "Server回显# " << buffer << std::endl;
  27. }
  28. else
  29. {
  30. break;
  31. }
  32. }
  33. }
  34. }

至此TCP通信的功能完成 -- 测试2

        但是至此,我们所写的不过是一个单进程版的,所以会出现下面的情况,后续需要进一步修改成为多进程形式的

全部代码

log.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. // 定义五种不同的信息
  5. #define DEBUG 0
  6. #define NORMAL 1
  7. #define WARNING 2
  8. #define ERROR 3 //一种不影响服务器的错误
  9. #define FATAL 4 //致命错误
  10. void logMessage(int level, const std::string message)
  11. {
  12. // 格式如下
  13. // [日志等级] [时间戳/时间] [pid] [message]
  14. // [FATAL0] [2023-06-11 16:46:07] [123] [创建套接字失败]
  15. // 暂定
  16. std::cout << message << std::endl;
  17. }

makefile

  1. cc=g++
  2. .PHONY:all
  3. all:tcpserver tcpclient
  4. tcpclient:tcpClient.cc
  5. $(cc) -o $@ $^ -std=c++11
  6. tcpserver:tcpServer.cc
  7. $(cc) -o $@ $^ -std=c++11
  8. .PHONY:clean
  9. clean:
  10. rm -f tcpserver tcpclient

tcpClient.cc

  1. #include "tcpClient.hpp"
  2. #include <memory>
  3. using namespace std;
  4. static void Usage(string proc)
  5. {
  6. cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
  7. }
  8. // ./tcpclient serverip serverport 调用逻辑
  9. int main(int argc, char *argv[])
  10. {
  11. if(argc != 3)
  12. {
  13. Usage(argv[0]);
  14. exit(1);
  15. }
  16. string serverip = argv[1];
  17. uint16_t serverport = atoi(argv[2]);
  18. unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));
  19. tcli->initClient();
  20. tcli->start();
  21. return 0;
  22. }

tcpClient.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. #include <sys/socket.h>
  8. #include <netinet/in.h>
  9. #include <arpa/inet.h>
  10. #include <unistd.h>
  11. #define NUM 1024
  12. class TcpClient
  13. {
  14. public:
  15. TcpClient(const std::string &serverip, const uint16_t &port)
  16. : _sock(1), _serverip(serverip), _serverport(port)
  17. {
  18. }
  19. void initClient()
  20. {
  21. // 1. 创建socket
  22. _sock = socket(AF_INET, SOCK_STREAM, 0);
  23. if (_sock < 0)
  24. {
  25. // 客户端也可以有日志,不过这里就不再实现了,直接打印错误
  26. std::cout << "socket create error" << std::endl;
  27. exit(2);
  28. }
  29. // 2. tcp的客户端要不要bind? 要的! 但是不需要显示bind,这里的client port要让OS自定!
  30. // 3. 要不要listen? -- 不需要!客户端不需要建立链接
  31. // 4. 要不要accept? -- 不要!
  32. // 5. 要什么? 要发起链接!
  33. }
  34. void start()
  35. {
  36. struct sockaddr_in server;
  37. memset(&server, 0, sizeof(server));
  38. server.sin_family = AF_INET;
  39. server.sin_port = htons(_serverport);
  40. server.sin_addr.s_addr = inet_addr(_serverip.c_str());
  41. if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
  42. {
  43. std::cerr << "socket connect error" << std::endl;
  44. }
  45. else
  46. {
  47. std::string msg;
  48. while (true)
  49. {
  50. std::cout << "Enter# ";
  51. std::getline(std::cin, msg);
  52. write(_sock, msg.c_str(), msg.size());
  53. char buffer[NUM];
  54. int n = read(_sock, buffer, sizeof(buffer) - 1);
  55. if (n > 0)
  56. {
  57. // 目前我们把读到的数据当成字符串, 截至目前
  58. buffer[n] = 0;
  59. std::cout << "Server回显# " << buffer << std::endl;
  60. }
  61. else
  62. {
  63. break;
  64. }
  65. }
  66. }
  67. }
  68. ~TcpClient()
  69. {
  70. if(_sock >= 0) close(_sock); //不写也行,因为文件描述符的生命周期随进程,所以进程退了,自然也就会自动回收了
  71. }
  72. private:
  73. int _sock;
  74. std::string _serverip;
  75. uint16_t _serverport;
  76. };

tcpServer.cc

  1. #include "tcpServer.hpp"
  2. #include <memory>
  3. using namespace server;
  4. using namespace std;
  5. static void Usage(string proc)
  6. {
  7. cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
  8. }
  9. // tcp服务器,启动上和udp server一模一样
  10. // ./tcpserver local_port
  11. int main(int argc, char *argv[])
  12. {
  13. if (argc != 2)
  14. {
  15. Usage(argv[0]);
  16. exit(USAGE_ERR);
  17. }
  18. uint16_t port = atoi(argv[1]);
  19. unique_ptr<TcpServer> tsvr(new TcpServer(port));
  20. tsvr->initServer();
  21. tsvr->start();
  22. return 0;
  23. }

tcpServer.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <cstdlib>
  6. #include <sys/types.h>
  7. #include <sys/socket.h>
  8. #include <sys/socket.h>
  9. #include <netinet/in.h>
  10. #include <arpa/inet.h>
  11. #include <unistd.h>
  12. #include "log.hpp"
  13. namespace server
  14. {
  15. enum
  16. {
  17. USAGE_ERR = 1,
  18. SOCKET_ERR,
  19. BIND_ERR,
  20. LISTEN_ERR
  21. };
  22. static const uint16_t gport = 8080;
  23. static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万
  24. class TcpServer
  25. {
  26. public:
  27. TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
  28. {
  29. }
  30. void initServer()
  31. {
  32. // 1. 创建socket文件套接字对象 -- 流式套接字
  33. _listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0
  34. if (_listensock < 0)
  35. {
  36. logMessage(FATAL, "create socket error");
  37. exit(SOCKET_ERR);
  38. }
  39. logMessage(NORMAL, "create socket success");
  40. // 2.bind绑定自己的网路信息 -- 注意包含头文件
  41. struct sockaddr_in local;
  42. memset(&local, 0, sizeof(local));
  43. local.sin_family = AF_INET;
  44. local.sin_port = htons(_port); // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到
  45. local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
  46. if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
  47. {
  48. logMessage(FATAL, "bind socket error");
  49. exit(BIND_ERR);
  50. }
  51. logMessage(NORMAL, "bind socket success");
  52. // 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程
  53. if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑
  54. {
  55. logMessage(FATAL, "listen socket error");
  56. exit(LISTEN_ERR);
  57. }
  58. logMessage(NORMAL, "listen socket success");
  59. }
  60. void start()
  61. {
  62. for (;;) // 一个死循环
  63. {
  64. // 4. server 获取新链接
  65. // sock 和client 进行通信的fd
  66. struct sockaddr_in peer;
  67. socklen_t len = sizeof(peer);
  68. int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
  69. if (sock < 0)
  70. {
  71. logMessage(ERROR, "accept error, next"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了
  72. continue;
  73. }
  74. logMessage(NORMAL, "accept a new link success");
  75. std::cout << "sock: " << sock << std::endl;
  76. // 5. 这里就是一个sock, 未来通信我们就用这个sock, 面向字节流的,后续全部都是文件操作!
  77. // 我们就可以直接使用read之类的面向字节流的操作都行
  78. // version 1
  79. serviceIO(sock);
  80. close(sock); // 走到这里就说明客户端已经关闭
  81. // 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符会越来越少,因为文件描述符本质就是一个数组下标
  82. // 只要是数组下标就会有尽头,提供服务的上限 就等于文件描述符的上限
  83. // 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
  84. }
  85. }
  86. void serviceIO(int sock)
  87. {
  88. // 先用最简单的,读取再写回去
  89. char buffer[1024];
  90. while (true)
  91. {
  92. ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
  93. if (n > 0)
  94. {
  95. // 截至目前,我们把读到的数据当作字符串
  96. buffer[n] = 0;
  97. std::cout << "recv message: " << buffer << std::endl;
  98. std::string outbuffer = buffer;
  99. outbuffer += "server[echo]";
  100. write(sock, outbuffer.c_str(), outbuffer.size()); // 在多路转接的时候再详谈write的返回值
  101. }
  102. else if (n == 0)
  103. {
  104. // 代表client退出 -- 把它想象成一个建立好的管道,客户端不写了,并且把它的文件描述符关了,读端就会像管道一样读到 0 TCP同理
  105. logMessage(NORMAL, "client quit, me too!");
  106. break;
  107. }
  108. }
  109. }
  110. ~TcpServer() {}
  111. private:
  112. int _listensock; // 修改二:改为listensock 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
  113. uint16_t _port;
  114. };
  115. } // namespace server

转下文:简单的TCP网络程序·多进程、多线程(后端服务器)_

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

闽ICP备14008679号