《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

相关文章

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu2289(简单二分)

虽说是简单二分,但是我还是wa死了  题意:已知圆台的体积,求高度 首先要知道圆台体积怎么求:设上下底的半径分别为r1,r2,高为h,V = PI*(r1*r1+r1*r2+r2*r2)*h/3 然后以h进行二分 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#includ

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

usaco 1.3 Prime Cryptarithm(简单哈希表暴搜剪枝)

思路: 1. 用一个 hash[ ] 数组存放输入的数字,令 hash[ tmp ]=1 。 2. 一个自定义函数 check( ) ,检查各位是否为输入的数字。 3. 暴搜。第一行数从 100到999,第二行数从 10到99。 4. 剪枝。 代码: /*ID: who jayLANG: C++TASK: crypt1*/#include<stdio.h>bool h

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount