《UNP》随笔——实现一个简单的回射服务器

2023-11-23 19:59

本文主要是介绍《UNP》随笔——实现一个简单的回射服务器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 1.什么是回射服务器
    • 2.服务器程序
    • 3.客户端程序
    • 4.并行服务器的轮廓
    • 5. 拓展(观察连接状态等)
    • 6.总结
    • 7.后端监听ip地址的三种主要的方式
        • 监听127.0.0.1
        • 监听0.0.0.0
        • 监听主机ip 192.168.0.113
        • 总结

1.什么是回射服务器

回射服务器的执行步骤:
(1)客户从标准输入读入一行文本,并写给服务器。
(2)服务器从网络输入读入这行文本,并回射给客户;
(3)客户从网络输入读入这行文本,并显示在标准输出上。
在这里插入图片描述

2.服务器程序

// 5.2 TCP回射服务器程序 
#include	"unp.h"
void str_echo(int sockfd)
{ssize_t		n;char		buf[MAXLINE];again:while ( (n = read(sockfd, buf, MAXLINE)) > 0)Writen(sockfd, buf, n);if (n < 0 && errno == EINTR)goto again;else if (n < 0)err_sys("str_echo: read error");
}int main(int argc, char **argv){int listenfd, connfd;pid_t childpid;socklen_t clilen;struct sockaddr_in  cliaddr, servaddr;listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 大端字节序,INADDR_ANY表示通配地址, servaddr.sin_port        = htons(SERV_PORT); // 小端字节序,服务器众所周知的端口,在unp中为9877Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));Listen(listenfd, LISTENQ); // LISTENQ 表示监听队列中最大容量,unp中定义为1024for( ; ; ){clilen = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen );if((childpid = Fork()) == 0){Close(listenfd); // 子进程需要关闭监听套接字str_echo(connfd);exit(0);}Close(connfd); //父进程关闭已连接套接字}
}

流程:①定义好socket的地址address。②Bind函数将地址和socket绑定起来。③使用Listen函数,监听socket信号。④利用死循环和子进程,处理每一个客户端的连接请求。

需要注意的点:

  • INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。也叫做通配地址, 其作用就是告诉主机,该socket可以接受目的地址为任何本地接口的连接。(经测试,无法使用局域网其他主机进行连接)
  • Accept函数,用于从已完成连接队列队头返回下一个已完成连接,如果已完成连接队列为空,那么进程将置于休眠状态(一般嵌套字socket会默认为阻塞状态)。
  • 并发服务器的程序轮廓。当服务器可能要接受多个主机的连接请求时,最简单的方法就是fork一个子进程来服务每个客户。子进程关闭监听套接字,父进程关闭已连接套接字,并等待其他连接。(原因在第4节解释)

3.客户端程序

#include	"unp.h"
void
str_cli(FILE *fp, int sockfd)
{char	sendline[MAXLINE], recvline[MAXLINE];// Fgets 将会调用fgets函数,读取从键盘输入的数据while (Fgets(sendline, MAXLINE, fp) != NULL) {// 会调用write函数,将该行数据发送给客户端Writen(sockfd, sendline, strlen(sendline));// 调用read函数,接受由服务器返回的数据,并用fputs,将其显示if (Readline(sockfd, recvline, MAXLINE) == 0)err_quit("str_cli: server terminated prematurely");Fputs(recvline, stdout);}
}int main(int argc, char **argv)
{int					sockfd;struct sockaddr_in	servaddr;if (argc != 2)err_quit("usage: tcpcli <IPaddress>");sockfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);  // 端口使用unp的默认端口 9877Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); // 将指定的ip argv[1] 放入sin_addr中, Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); // 发起连接申请str_cli(stdin, sockfd);		/* do it all */exit(0);
}

4.并行服务器的轮廓

在这里插入图片描述

疑问点:为什么在子进程中就要关闭监听套接字listenfd? 为什么已连接套接字connfd关闭了两次?

用图解来解释:
在这里插入图片描述
此时有一连接请求,服务器接受该请求,并创建新的套接字connfd,这是一个已连接套接字,可以使用该套接字来跨连接读写数据。如下图:
在这里插入图片描述
下一步就是调用fork创建子进程,如下图
在这里插入图片描述
可以看到,子进程把父进程的监听套接字listenfd和连接套接字connfd都复制了一份。但是,我们创建子进程的目的就是为了处理已连接套接字,如果一直保持两个监听套接字,当有新的连接进行过来时,监听队列会出现冗余的连接套接字,同样,我们也希望父进程只处理监听套接字。 因此,父进程关闭已连接套接字,子进程关闭监听套接字。
在这里插入图片描述
这就是两个套接字最终的状态。父进程与子进程各司其职,子进程处理当前的已连接套接字,进行跨连接读写操作,父进程继续处理下一客户连接,创建新的子进程来处理新的连接。

  • 为什么已连接套接字关闭了两次?
    每个文件或者套接字,都有一个引用计数,它是当前打开着的引用该文件或套接字的描述符的个数。当listenfd和connfd从函数中返回时,其引用计数都是1,但是调用了fork,创建子进程时,这两个描述符也在父进程子进程中共享,因此与这两个套接字相关联的文件表项各自的引用计数都为2。这样的话,当子进程执行close()的时候,引用计数由2减为1,并不会被真正的清理和资源释放。(子进程结束的时候,也会自动对未执行引用计数减一的描述符来执行引用减一,因此close(connfd)常常略写)

5. 拓展(观察连接状态等)

  1. 观察IP报文
    在这里插入图片描述
    客户端输入的是asd,可以在报文中看到asd的ASCII字符,也就是包含的数据信息。同时也是TCP传输信息的过程,发送方发送一个信息报文,接受方返回一个确认报文
  2. 查看端口信息
losf -i:端口号

在这里插入图片描述

  1. 查看进程间的关系
ps -t pts/5 -t pts/7 -o pid,ppid,tty,stat,args,wchan
  • pts表示打开的终端
    在这里插入图片描述
  • wait_woken就是在休眠(一般是阻塞状态),等待唤醒。
  • PID=5045的进程,状态是Z,也就是僵死状态,是因为服务器子进程终止时,会给父进程发送一个SIGCHLD信号,但是本程序没有在代码中捕获这一信号,该信号默认被忽略,所以导致子进程僵死。(僵死进程同样会消耗系统资源,所以需要对其进行处理)

6.总结

  1. 如何设计一个可以监听局域网的服务端?
    可以,只需要将监听地址进行修改即可,修改为本地的IP地址A,这样其他主机的服务器即可通过A:端口,来进行服务器请求。

不需要修改任何代码,只需要修改防火墙即可让其他主机连接。这一点涉及到后端监听ip的三种主要方式,相关内容往下查看

    servaddr.sin_family = AF_INET;const char* ip = argv[1];inet_pton(AF_INET, ip, &servaddr.sin_addr); // 修改这里进行绑定本地地址即可//servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 大端字节序,INADDR_ANY表示通配地址servaddr.sin_port        = htons(SERV_PORT); // 小端字节序,服务器众所周知的端口,在unp中为9877Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));Listen(listenfd, LISTENQ); // LISTENQ 表示监听队列中最大容量,unp中定义为1024

设置过程中遇到的问题:

  • 主机B上监听9877端口,主机A不能通过主机B的IP地址和这个端口进行连接,发送了TCP请求报文,但是主机B没有回复报文。(利用tcodump,可以看到主机B收到了该请求报文)
  • 为了验证是否是程序的问题,将服务端程序换到主机A上,主机A上监听9877端口,然后主机B可以通过这个端口建立TCP连接。说明程序没有问题
  • 最后发现是防火墙的原因,主机A关闭了防火墙,而主机B开启了,所以主机B不会回复主机A的连接请求。
sudo ufw status
# 查看防火墙状态
sudo ufw allow 9877  
#允许外部访问9877端口sudo ufw delete allow 9877 
#禁止外部访问9877 端口

7.后端监听ip地址的三种主要的方式

  • 监听127.0.0.1
  • 监听0.0.0.0
  • 监听主机IP地址
监听127.0.0.1

127.0.0.1也称为回环地址。该地址指电脑本身,主要预留测试本机的TCP/IP协议是否正常。只要使用这个地址发送数据,则数据包不会出现在网络传输过程中。

  1. 本机通过127.0.0.1访问成功,网络接口为loopback
  2. 本机通过局域网IP 192.168.0.113访问失败,网络接口为loopback
  3. 同一局域网下的外部主句通过局域网IP 192.168.0.112访问失败,网络接口-et

因此,在实际应用中,我们在服务端监听ip地址的时候不要绑定到127.0.0.1,如果绑定到了127.0.0.1,会导致我们的应用只能在本地127.0.0.1访问,其他人无法通过其他任何方式进行访问

监听0.0.0.0

0.0.0.0也称作是通配地址,用INADDR_ANY指代。在IPv4中,0.0.0.0地址被用于表示一个无效的,未知的或者不可用的目标。在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。

  1. 本机通过127.0.0.1访问成功,网络接口为loopback
  2. 本机通过局域网IP 192.168.0.113 访问成功,网络接口为loopback
  3. 同一局域网下的外部主句通过局域网IP 192.168.0.112访问成功,网络接口-et1

比如我有一台服务器,一个外网A,一个内网B,如果我绑定的端口指定了0.0.0.0,那么通过内网地址或外网地址都可以访问我的应用。

监听主机ip 192.168.0.113
  1. 本机通过127.0.0.1访问失败,网络接口为loopback
  2. 本机通过局域网IP 192.168.0.113 访问成功,网络接口为loopback
  3. 同一局域网下的外部主句通过局域网IP 192.168.0.112访问成功,网络接口-et1
总结

在实际应用中,最好的监听ip地址方式为:监听到0.0.0.0,这样本机、局域网内其他主机,均可以访问该主机。

参考https://segmentfault.com/a/1190000018629247

这篇关于《UNP》随笔——实现一个简单的回射服务器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/420368

相关文章

Spring StateMachine实现状态机使用示例详解

《SpringStateMachine实现状态机使用示例详解》本文介绍SpringStateMachine实现状态机的步骤,包括依赖导入、枚举定义、状态转移规则配置、上下文管理及服务调用示例,重点解... 目录什么是状态机使用示例什么是状态机状态机是计算机科学中的​​核心建模工具​​,用于描述对象在其生命

Spring Boot 结合 WxJava 实现文章上传微信公众号草稿箱与群发

《SpringBoot结合WxJava实现文章上传微信公众号草稿箱与群发》本文将详细介绍如何使用SpringBoot框架结合WxJava开发工具包,实现文章上传到微信公众号草稿箱以及群发功能,... 目录一、项目环境准备1.1 开发环境1.2 微信公众号准备二、Spring Boot 项目搭建2.1 创建

IntelliJ IDEA2025创建SpringBoot项目的实现步骤

《IntelliJIDEA2025创建SpringBoot项目的实现步骤》本文主要介绍了IntelliJIDEA2025创建SpringBoot项目的实现步骤,文中通过示例代码介绍的非常详细,对大家... 目录一、创建 Spring Boot 项目1. 新建项目2. 基础配置3. 选择依赖4. 生成项目5.

Linux下删除乱码文件和目录的实现方式

《Linux下删除乱码文件和目录的实现方式》:本文主要介绍Linux下删除乱码文件和目录的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux下删除乱码文件和目录方法1方法2总结Linux下删除乱码文件和目录方法1使用ls -i命令找到文件或目录

SpringBoot+EasyExcel实现自定义复杂样式导入导出

《SpringBoot+EasyExcel实现自定义复杂样式导入导出》这篇文章主要为大家详细介绍了SpringBoot如何结果EasyExcel实现自定义复杂样式导入导出功能,文中的示例代码讲解详细,... 目录安装处理自定义导出复杂场景1、列不固定,动态列2、动态下拉3、自定义锁定行/列,添加密码4、合并

mybatis执行insert返回id实现详解

《mybatis执行insert返回id实现详解》MyBatis插入操作默认返回受影响行数,需通过useGeneratedKeys+keyProperty或selectKey获取主键ID,确保主键为自... 目录 两种方式获取自增 ID:1. ​​useGeneratedKeys+keyProperty(推

Spring Boot集成Druid实现数据源管理与监控的详细步骤

《SpringBoot集成Druid实现数据源管理与监控的详细步骤》本文介绍如何在SpringBoot项目中集成Druid数据库连接池,包括环境搭建、Maven依赖配置、SpringBoot配置文件... 目录1. 引言1.1 环境准备1.2 Druid介绍2. 配置Druid连接池3. 查看Druid监控

Linux在线解压jar包的实现方式

《Linux在线解压jar包的实现方式》:本文主要介绍Linux在线解压jar包的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux在线解压jar包解压 jar包的步骤总结Linux在线解压jar包在 Centos 中解压 jar 包可以使用 u

c++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被

Qt使用QSqlDatabase连接MySQL实现增删改查功能

《Qt使用QSqlDatabase连接MySQL实现增删改查功能》这篇文章主要为大家详细介绍了Qt如何使用QSqlDatabase连接MySQL实现增删改查功能,文中的示例代码讲解详细,感兴趣的小伙伴... 目录一、创建数据表二、连接mysql数据库三、封装成一个完整的轻量级 ORM 风格类3.1 表结构