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

编程入门 行业动态 更新时间:2024-10-12 03:27:22

《UNP》<a href=https://www.elefans.com/category/jswz/34/1769507.html style=随笔——实现一个简单的回射服务器"/>

《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,这样本机、局域网内其他主机,均可以访问该主机。

参考

更多推荐

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

本文发布于:2024-02-06 09:30:29,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1748252.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:随笔   简单   服务器   UNP

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!