• Linux——网络套接字2|Tcp服务器编写


    本篇博客先看后面的代码,再回来看上面这些内容。

    .hpp文件,基本调用

    服务器基本框架

    接下来用tcp的方式创建套接字,我们用SOCK_STREAM

    因为TCP是面向连接的,所以当我们通信的时候需要建立连接,

    listen

    将套接字状态转为监听状态,第一个参数套接字,第二个参数全连接队列长度(后续会详解这个参数)。

    成功返回0,否则返回-1

    如何理解监听状态?

    由于TCP通信是需要连接的,即如果对方发过来连接我们要即使进行接收,listen是监听来自客户端的tcp socket的连接请求,即listen是看看客户端有没有发送连接请求过来。

    我们在tcp_server.hpp里面完成创建tcp时初始化服务器的三大步:1.创建套接2.绑定3.listen

    当我们启动服务器之后,用netstat -antp 可查看tcp相关的一些服务,加l之后,只会显示监听状态的服务,下图中listen表示当前服务器处于监听状态,即此时服务器随时等待别人的请求。

    当服务器启动之后要先获取连接,有人连服务器时,服务器才能获取连接,若没人连服务器,服务器就处于阻塞状态,这里我们要这个接口。

    accept

    获取新连接,第一个参数是套接字,第二个参数是输出型参数,第三个参数是输入输出型参数。

    accept成功之后返回一个套接字,失败返回-1

    后俩个参数和recvfrom后俩个参数含义一模一样,代表的是客户端的ip和客户端的端口号

    accept的返回值sock和我们自己的_sock有何区别?

    _sock的核心任务只有一个:获取新连接。而未来真正进行网络服务的是sock。_sock只是帮助底层的accept把底层的连接获取上来。这个_sock我们更倾向于称作listensock,也俗称监听套接字。上图里得sock一般成为servicesock,服务套接字。

    _sock是通过accept获取新的连接,sock负责网络服务。

    read|write

    当服务器创建连接成功后,服务器需要读取对方发送过来的信息,注意:这里不能用recvfrom这个专门用于udp报文,是面向数据报的,我们用下面这个接口。这个就是我们之前在OS经常用到的接口。

    单进程循环版

    我们可用telnet+IP+端口号去连接我们的服务器

    之后按ctrl+],就可以发消息,此时发送的消息会被服务器收到

    telnet的发送完信息之后,会立马读取对方发过来的信息,我们这里让服务器收到信息后,把信息又返回给了对方,因此这里左边消息出现了俩次。

    telnet退出的时候输入按ctrl+],之后输入quit.此时telnet会自动退出,根据我们所写的程序,服务器也会自动退出。

    当我们创建俩个客户端,给服务器发信息时,第二个客户端发不出去。

    当第一个客户端退出之后,第二个客户端发送的消息会立刻被服务器接收。

    这种情况又如何解决呢?这里我们写的是单进程,而且service里面是死循环,死循环一直在进行读取,若不退出就一直在读写。我们可以写一个多进程版

    创建子进程,让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?

    能,创建子进程时,子进程会继承父进程的文件描述符表,所以会和父进程看到同一个文件。

    这里让父进程创建连接,子进程去给客户端提供服务。

    但是当子进程退出之后会进入僵尸状态,父进程需要回收子进程,而父进程在等待子进程时是阻塞时等待,子进程不退出,父进程wait就不会返回,但由于这里有多个客户端,就不能使用waitpid,我们需要一种不阻塞式的等待子进程。如果这里子进程较多,用waitpid非阻塞等待比较麻烦。

    子进程退出时会向父进程发送SIGCHILD信号,如果主动忽略SIGCHILE信号,子进程退出的时候会自动释放自己的僵尸状态。

    父进程至少打开了这俩个套接字,即俩个文件描述符。

    创建了子进程,子进程会继承父进程打开的文件与文件fd。

    这里子进程是来进行提供服务的,不需要知道listensock,只需要servicesock,因此要让子进程关闭掉不需要的listensock,对于父进程只需要关闭自己不需要的servicesock套接字。父进程永远只保留listensock即可。

    tcp_server.hpp

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #include
    14. #include"log.hpp"
    15. static void service(int sock,const std::string&clientip,const uint16_t&clientport)
    16. {
    17. //收到对方的消息后,进行一些转换再返回给对方
    18. char buffer[1024];
    19. while(true)
    20. {
    21. //read和write可以直接被使用!
    22. ssize_t s=read(sock,buffer,sizeof(buffer)-1);
    23. if(s>0)
    24. {
    25. buffer[s]=0;
    26. //将发过来的数据当作字符串
    27. std::cout<":"<"# "<
    28. }
    29. else if(s==0)
    30. //返回值为0,代表对方关闭了连接,啥都没读到
    31. {
    32. logMessage(NORMAL,"%s:%d shutdown,me too!",clientip.c_str(),clientport);
    33. break;
    34. }
    35. else
    36. {
    37. logMessage(ERROR,"read socket error,%d:%s",errno,strerror(errno));
    38. break;
    39. }
    40. //走到这就读取成功了,我们接下来向套接字当中写入
    41. write(sock,buffer,strlen(buffer));
    42. }
    43. }
    44. class TcpServer
    45. {
    46. private:
    47. const static int gbacklog=20;//这是listen的第二个参数
    48. //一般不能太大也不能太小,后面会解释,这时listen的第二个参数
    49. public:
    50. TcpServer(uint16_t port,std::string ip=""):listensock(-1),_port(port),_ip(ip)
    51. {}
    52. //初始化服务器
    53. void initServer()
    54. {
    55. listensock=socket(AF_INET,SOCK_STREAM,0);//返回值照样是文件描述符
    56. //参数含义第一个:网络通信 第二个:流式通信
    57. if(listensock<0)//创建套接字失败
    58. {
    59. logMessage(FATAL,"create socket error,%d:%s",errno,strerror(errno));//打印报错信息
    60. exit(2);
    61. }
    62. logMessage(NORMAL,"create socket success,sock:%d",listensock);//打印套接字,它的文件描述符是3
    63. //到这里创建套接字成功,接下来进行bind
    64. //bind目的是让IP和端口进行绑定
    65. //我们需要套接字,和sockaddr(这个里面包含家族等名称)
    66. //绑定——文件和网络
    67. struct sockaddr_in local;
    68. memset(&local,0,sizeof local);//初始化local
    69. local.sin_family=AF_INET;
    70. local.sin_port=htons(_port);//端口号,端口是主机序列,我们需要主机转网络
    71. local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());
    72. //IP地址,由于我们构造的时候是IP是个空的字符串
    73. //所以我们可以绑定任意IP
    74. //我们一般推荐绑定0号地址或特殊IP
    75. //填充的时候IP是空的,就用INADDR_ANY否则用inet_addr
    76. if(bind(listensock,(struct sockaddr*)&local,sizeof local)<0)
    77. {
    78. //走到这就绑定失败了,我们打印错误信息
    79. logMessage(FATAL,"bind error,%d:%s",errno,strerror(errno));
    80. exit(3);
    81. }
    82. //因为TCP是面向连接的,当我们正式通信的时候需要先建立连接。
    83.             //listen是在等待对方发过来连接,我们直接接收
    84. if(listen(listensock,gbacklog)<0)
    85. {
    86. logMessage(FATAL,"listen error,%d:%s",errno,strerror(errno));
    87. exit(4);
    88. }
    89. logMessage(NORMAL,"init server success");
    90. }
    91. //启动服务器
    92. void start()
    93. {
    94. signal(SIGCHLD,SIG_IGN);//子进程自动释放自己的僵尸状态,用信号可以避免父进程阻塞等待子进程
    95.             //子进程退出,会向父进程发SIGCHLD信号,对SIGCHLD信号主动忽略,子进程退出的时候,会自动释放自己的僵尸状态
    96. //服务器一旦启动就要周而复始的去运行
    97. while(true)
    98. {
    99. //服务器启动之后先获取连接
    100. //当有人连服务器的时候才能获取连接,若没人连就无法获取
    101. //这里当没人连服务器的时候,我们让服务器一直进行阻塞。
    102. struct sockaddr_in src;
    103. socklen_t len=sizeof(src);
    104. int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
    105. if(servicesock<0)
    106. {
    107. //获取连接失败
    108. logMessage(ERROR,"accept error,%d:%s",errno,strerror(errno));
    109. continue;
    110. }
    111. //获取连接成功了
    112. uint16_t client_port=ntohs(src.sin_port);
    113. //客户端端口号在src
    114. //由于是网络发送过来得套接字信息
    115. //所以要把信息进行网络转主机
    116. std::string client_ip=inet_ntoa(src.sin_addr);
    117. //我们需要将四字节网络序列的IP地址,转换成字符串风格的点分十进制的IP地址
    118. //到这里我们拿到了IP和端口号
    119. //谁连接服务器,服务器就拿到谁的信息
    120. logMessage(NORMAL,"link success,servicesock:%d | %s : %d| \n",servicesock,client_ip.c_str(),client_port);
    121. //这里servicesock是4,符合数组下标描述符的分配规则
    122. //接下来进行通信服务
    123. //verison1--单进程循环版——智能一次处理一个客户端,处理完一个客户端之后,才能处理下一个客户端
    124. //service(servicesock,client_ip,client_port);
    125. //version——2.1多进程版
    126. //创建子进程,让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?
    127. pid_t id=fork();
    128. assert(id!=-1);
    129. if(id==0)
    130. {
    131. //子进程
    132. close(listensock);
    133.                     //子进程只是来进行提供服务的,我们关闭它不需要的套接字,同理父进程也关闭自己不需要的套接字
    134. service(servicesock,client_ip,client_port);//子进程开始提供服务
    135. exit(0);//子进程退出进入僵尸状态
    136. }
    137. //父进程要回收子进程
    138. close(servicesock);
    139. }
    140. }
    141. ~TcpServer(){}
    142. private:
    143. uint16_t _port;//端口号
    144. std::string _ip;//ip地址
    145. int listensock;
    146. };

    tcp_server.cc

    1. #include "tcp_server.hpp"
    2. #include
    3. static void usage(std::string proc)
    4. {
    5. std::cout << "\nUsage: " << proc << " port\n" << std::endl;
    6. }
    7. // ./tcp_server port
    8. int main(int argc, char *argv[])
    9. {
    10. if(argc != 2)
    11. {
    12. usage(argv[0]);
    13. exit(1);
    14. }
    15. uint16_t port = atoi(argv[1]);
    16. std::unique_ptr svr(new TcpServer(port));
    17. svr->initServer();
    18. svr->start();
    19. return 0;
    20. }

    makefile

    1. .PHONY:all
    2. all:tcp_client tcp_server
    3. tcp_client:tcp_client.cc
    4. g++ -o $@ $^ -std=c++11 #-lpthread
    5. tcp_server:tcp_server.cc
    6. g++ -o $@ $^ -std=c++11
    7. .PHONY:clean
    8. clean:
    9. rm -f tcp_client tcp_server

    log.hpp

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. // 日志是有日志级别的
    8. #define DEBUG 0
    9. #define NORMAL 1
    10. #define WARNING 2
    11. #define ERROR 3
    12. #define FATAL 4
    13. const char *gLevelMap[] = {
    14. "DEBUG",
    15. "NORMAL",
    16. "WARNING",
    17. "ERROR",
    18. "FATAL"
    19. };
    20. #define LOGFILE "./threadpool.log"
    21. // 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
    22. void logMessage(int level, const char *format, ...)
    23. {
    24. #ifndef DEBUG_SHOW
    25. if(level== DEBUG) return;
    26. #endif
    27. // va_list ap;
    28. // va_start(ap, format);
    29. // while()
    30. // int x = va_arg(ap, int);
    31. // va_end(ap); //ap=nullptr
    32. char stdBuffer[1024]; //标准部分
    33. time_t timestamp = time(nullptr);
    34. // struct tm *localtime = localtime(×tamp);
    35. snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
    36. char logBuffer[1024]; //自定义部分
    37. va_list args;
    38. va_start(args, format);
    39. // vprintf(format, args);
    40. vsnprintf(logBuffer, sizeof logBuffer, format, args);
    41. va_end(args);
    42. // FILE *fp = fopen(LOGFILE, "a");
    43. printf("%s%s\n", stdBuffer, logBuffer);
    44. // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    45. // fclose(fp);
    46. }
  • 相关阅读:
    辛普森一家,有趣的句子
    git rebase 使用详解
    计算机视觉与深度学习-经典网络解析-ZFNet-[北邮鲁鹏]
    【小尘送书-第八期】《小团队管理:如何轻松带出1+1>2的团队》
    计算机内存与外存的区别及使用配合(内存外存区别与搭配;快速缓存;计算机总线结构)
    算法通过村第六关-树青铜笔记|中序后序
    财务RPA机器人如何使用
    uboot源码分析(基于S5PV210)之uboot的命令体系与环境变量
    【Java Web】Servlet规范讲解
    VScode SSH无法免密登录
  • 原文地址:https://blog.csdn.net/weixin_49449676/article/details/129647885