1.什么是回射服务器
回射服务器的执行步骤: (1)客户从标准输入读入一行文本,并写给服务器。 (2)服务器从网络输入读入这行文本,并回射给客户; (3)客户从网络输入读入这行文本,并显示在标准输出上。
2.服务器程序
#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);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for( ; ; ){
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];
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Writen(sockfd, sendline, strlen(sendline));
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);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd);
exit(0);
}
4.并行服务器的轮廓
疑问点:为什么在子进程中就要关闭监听套接字listenfd? 为什么已连接套接字connfd关闭了两次?
用图解来解释: 此时有一连接请求,服务器接受该请求,并创建新的套接字connfd,这是一个已连接套接字,可以使用该套接字来跨连接读写数据。如下图: 下一步就是调用fork创建子进程,如下图 可以看到,子进程把父进程的监听套接字listenfd和连接套接字connfd都复制了一份。但是,我们创建子进程的目的就是为了处理已连接套接字,如果一直保持两个监听套接字,当有新的连接进行过来时,监听队列会出现冗余的连接套接字,同样,我们也希望父进程只处理监听套接字。 因此,父进程关闭已连接套接字,子进程关闭监听套接字。 这就是两个套接字最终的状态。父进程与子进程各司其职,子进程处理当前的已连接套接字,进行跨连接读写操作,父进程继续处理下一客户连接,创建新的子进程来处理新的连接。
- 为什么已连接套接字关闭了两次?
每个文件或者套接字,都有一个引用计数,它是当前打开着的引用该文件或套接字的描述符的个数。当listenfd和connfd从函数中返回时,其引用计数都是1,但是调用了fork,创建子进程时,这两个描述符也在父进程子进程中共享,因此与这两个套接字相关联的文件表项各自的引用计数都为2。这样的话,当子进程执行close()的时候,引用计数由2减为1,并不会被真正的清理和资源释放。(子进程结束的时候,也会自动对未执行引用计数减一的描述符来执行引用减一,因此close(connfd)常常略写)
5. 拓展(观察连接状态等)
- 观察IP报文
客户端输入的是asd,可以在报文中看到asd的ASCII字符,也就是包含的数据信息。同时也是TCP传输信息的过程,发送方发送一个信息报文,接受方返回一个确认报文 - 查看端口信息
losf -i:端口号
- 查看进程间的关系
ps -t pts/5 -t pts/7 -o pid,ppid,tty,stat,args,wchan
- pts表示打开的终端
- wait_woken就是在休眠(一般是阻塞状态),等待唤醒。
- PID=5045的进程,状态是Z,也就是僵死状态,是因为服务器子进程终止时,会给父进程发送一个SIGCHLD信号,但是本程序没有在代码中捕获这一信号,该信号默认被忽略,所以导致子进程僵死。(僵死进程同样会消耗系统资源,所以需要对其进行处理)
6.总结
- 如何设计一个可以监听局域网的服务端?
可以,只需要将监听地址进行修改即可,修改为本地的IP地址A,这样其他主机的服务器即可通过A:端口,来进行服务器请求。
不需要修改任何代码,只需要修改防火墙即可让其他主机连接。这一点涉及到后端监听ip的三种主要方式,相关内容往下查看
servaddr.sin_family = AF_INET;
const char* ip = argv[1];
inet_pton(AF_INET, ip, &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
设置过程中遇到的问题:
- 主机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
sudo ufw delete allow 9877
7.后端监听ip地址的三种主要的方式
- 监听127.0.0.1
- 监听0.0.0.0
- 监听主机IP地址
监听127.0.0.1
127.0.0.1也称为回环地址。该地址指电脑本身,主要预留测试本机的TCP/IP协议是否正常。只要使用这个地址发送数据,则数据包不会出现在网络传输过程中。
- 本机通过127.0.0.1访问成功,网络接口为loopback
- 本机通过局域网IP 192.168.0.113访问失败,网络接口为loopback
- 同一局域网下的外部主句通过局域网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地址都能够访问该服务。
- 本机通过127.0.0.1访问成功,网络接口为loopback
- 本机通过局域网IP 192.168.0.113 访问成功,网络接口为loopback
- 同一局域网下的外部主句通过局域网IP 192.168.0.112访问成功,网络接口-et1
比如我有一台服务器,一个外网A,一个内网B,如果我绑定的端口指定了0.0.0.0,那么通过内网地址或外网地址都可以访问我的应用。
监听主机ip 192.168.0.113
- 本机通过127.0.0.1访问失败,网络接口为loopback
- 本机通过局域网IP 192.168.0.113 访问成功,网络接口为loopback
- 同一局域网下的外部主句通过局域网IP 192.168.0.112访问成功,网络接口-et1
总结
在实际应用中,最好的监听ip地址方式为:监听到0.0.0.0,这样本机、局域网内其他主机,均可以访问该主机。
参考https://segmentfault.com/a/1190000018629247
|