UNIX网络编程卷1:套接字联网-第5章:TCP客户/服务器程序示例

时间:2021-05-14 10:59:39

1. 简单回射客户/服务器图解

UNIX网络编程卷1:套接字联网-第5章:TCP客户/服务器程序示例

2.代码实例

以下程序摘自UNIX网络编程,我会对其加上一些注解,方便阅读

TCP回射服务器程序:main函数

/*怎么编译运行可以查看我的另一篇博文:unpv13e编译和运行

/* tcpserv01.c */   
#include <sys/socket.h>
#include <strings.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int
main(int argc, char **argv)
{
int listenfd, connfd; //用来存储监听套接字fd和连接套接字fd
pid_t childpid; // 用来存储子进程fd
socklen_t clilen; //用来存储套接字地址大小
struct sockaddr_in cliaddr, servaddr; //用来存储服务端和客户端套接字地址

if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //创建使用ipv4,tcp的流套接字
{
perror("socket");
exit(1);
}

bzero(&servaddr, sizeof(servaddr)); //初始化服务端套接字地址为0,bzero已经过时,此处最好使用memset
servaddr.sin_family = AF_INET; //使用ipv4协议族
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //将主机字节序转换为网络字节序(默认大端字节序)并赋予相应值,INADDR_ANY表示接受任意地址的请求
servaddr.sin_port = htons(9877); //赋予服务端套接字地址结构中的端口值

if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) //将创建的套接字listenfd和服务端套接字地址结构绑定在一起
{
perror("bind");
exit(1);
}
if(listen(listenfd, 5) < 0) //监听端口收到的请求,最大连接数设置为5,调用listen后内核会维护两个队列,具体参见tcp套接字编程
{
perror("listen");
exit(1);
}

for(;;) //最常见的服务进程是被动的等待请求,除非手动关闭或者遇到异常,否则不会主动关闭
{
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) //若已连接队列为空,则服务进程阻塞于此,否则取出队列头,内核生成一个全新的描述符,cliaddr是值-结果参数
{
perror("accept");
exit(1);
}

if((childpid = fork()) < 0) //毫无悬念,创建子进程。注意,创建子进程后,对父进程现有套接字的引用都增1,表示子进程也可使用父进程拥有的套接字,此时注意close一些不是用的
{
perror("fork");
exit(1);
}
else if(childpid == 0) /* child process */ fork返回两次,返回0的表示处于子进程
{
if(close(listenfd) < 0) /* close listening socket */ /*关掉子进程对监听套接字的可使用权,监听套接字引用计数减1,直至计数为0,才回真正关闭套接字*/
{
perror("child close");
exit(1);
}
str_echo(connfd); /* process the request */ //传递连接套接字给服务器程序的业务处理函数,从而进行通信操作
exit(0); /*此处别忘了哟,子进程用完了要退出,要不然站着进程资源不释放,又没用,你猜会怎么样

}
if(close(connfd) < 0) /* parent close connected socket */ //父进程也要关闭连接套接字,(以为accept成功返回的时候,父进程也拥有了对连接套接字的使用权)
{
perror("parent close");
exit(1);
}
}
}

TCP回射服务器程序:str_echo函数

/* str_echo.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

void
str_echo(int sockfd)
{
ssize_t n;
char buf[4096]; //创建一个应用层缓冲区,用于存放从套接字中读到的数据
again:
while((n = read(sockfd, buf, 4096)) > 0)//如果套接字队列没有数据可读,则read处于阻塞状态
writen(sockfd, buf, n);

if(n < 0 && errno == EINTR) //read被信号中断,此时重新读
goto again;
else if(n < 0) //read发生错误,打印错误信息,并终止进程
{
perror("read");
exit(1);
}

}



TCP回射客户程序:main函数

/* tcpcli01.c */
#include <stdio.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>

int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;

if(argc != 2)
{
printf("usage: tcpcli <IPaddress> ");
exit(0);
}

if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //不管是服务程序还是客户程序,想要网络通信,都得先创建通信用的网络套接字
{
perror("socket"); //创建套接字失败的下场
exit(1);
}

bzero(&servaddr, sizeof(servaddr)); //同样的,初始化套接字地址结构为0
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) < 0)
{
perror("inet_pton");
exit(1);
}

if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) //调用connect会发起三路握手的连接过程
{
perror("connect");
exit(1);
}

str_cli(stdin, sockfd); /* do it all */ //调用客户端的业务处理程序,传入标准输入流(用于得到客户端的输入)和用于网络通信的套接字描述符

exit(0);
}


TCP回射客户程序:str_cli函数

/* str_cli.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void
str_cli(FILE *fp, int sockfd)
{
char sendline[4096], recvline[4096]; //应用程序创建一个发送缓冲区和接收缓冲区

while(fgets(sendline, 4096, fp) != NULL) //
{
writen(sockfd, sendline, strlen(sendline));

if(readline(sockfd, recvline, 4096) == 0)
{
printf("str_cli: server terminated prematurely");
exit(0);
}
fputs(recvline, stdout);
}
}

当然,上述程序还有诸多问题,我们只是以一个简单示例程序来展示tcp客户端和服务通信


改进的服务器程序:(增加了清除僵尸进程的机制和accept被中断重新调用的机制)

    #include    "unp.h"  

int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);

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, (SA *) &servaddr, sizeof(servaddr));

Listen(listenfd, LISTENQ);

Signal(SIGCHLD, sig_chld); /* must call waitpid() */

for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}

if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}

void sig_chld(int signo)  
{
pid_t pid;
int stat;

pid = wait(&stat); //
printf("child %d terminated\n",pid);
return;
}