您的位置:首页 > 理论基础 > 计算机网络

TCP客户/服务器程序示例

2017-10-29 21:07 471 查看

概述

简单的TCP回射服务器:客户从标准输入读入一行文本,并写给服务器;服务器从网络输入读取文本,并回射给客户;客户从网络输入读取文本,并显示在终端。

TCP回射程序

服务器端:

void server_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while((n = read(sockfd , buf , MAXLINE)) > 0){
Write(sockfd , buf , n);
}
if(n < 0 && errno == EINTR)//错误或者还有数据,重复读取
goto again;
else if(n < 0)
err_sys("server_echo: read error");
}
int main(int argc, char **argv)//客户程序
{
int                 listenfd, connfd;//监听fd和连接fd
struct sockaddr_in  servaddr , cliaddr;
socklen_t clilen;
pid_t childpid;

/*×××××××××××××××××××服务器端套路开始××××××××××××××××××××*/
listenfd = Socket(AF_INET, SOCK_STREAM, 0);//套接字创建
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family      = AF_INET;//服务器IP地址为网卡IP地址
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port        = htons(4099);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));//绑定本机IP地址和端口,设置结构体成员而已
Listen(listenfd, LISTENQ);//绑定之后就可以开始监听外部连接了,指定了排队最大客户连接数
/********************服务器端套路结束*********************/
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd , (SA *)&cliaddr , &clilen);//传入长度为了达到值-结果
if((childpid = Fork()) == 0){
Close(listenfd);//文件计数关闭一次
server_echo(connfd);
Close(connfd);
exit(0);
}//child,并行服务器就是不停fork处理
Close(connfd);//等待下一个客户机连接。
}
}


客户机端:

void str_cli(FILE *fp , int sockfd)
{
char sendline[MAXLINE] , recvline[MAXLINE];
while(fgets(sendline , MAXLINE , fp) != NULL){
Write(sockfd , sendline , strlen(sendline));
if(Readline(sockfd , recvline , MAXLINE) == 0)
err_quit("str_cli:server terminated ");
fputs(recvline , stdout);
}
}
int main(int argc, char **argv)//客户程序
{
int                 sockfd;
struct sockaddr_in  servaddr;

if (argc != 2)
err_quit("usage: a.out <IPaddress>");

if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)//创建一个网际字节流套接字,TCP套接字
err_sys("socket error");

bzero(&servaddr, sizeof(servaddr));//传递地址,将N字节清0
servaddr.sin_family = AF_INET;
servaddr.sin_port   = htons(4099);  /* daytime server */
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)//填入服务器IP地址和端口号13
err_quit("inet_pton error for %s", argv[1]);

if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)//TCP握手操作,交换数据
err_sys("connect error");

str_cli(stdin , sockfd);

exit(0);
}




认真分析过程很重要理解状态转换:

正常启动和终止分析结合状态转换图

1、服务器端后台运行启动,调用Socket , Bind ,Listen,然后阻塞在Accept,等待客户机连接。这时候服务器进入Listen状态。

2、运行客户机子程序,调用Socket,Connect发起三次握手操作。握手成功之后服务器和客户机全部进入ESTABLISHED状态,可以进行相互收发了。并且服务器从Accept返回,创建服务器子进程进入server_echo并行处理这个连接;客户机从Connect返回进入str_cli。另一方面服务器再次调用Accept并阻塞,等待下一个连接。





3、客户机输入Ctrl+D,表示终止。这时候客户机exit,发出一个FIN信号给服务器,并进入FIN_WAIT_1状态。服务器子进程收到信号之后,发送ACK信号给客户机,服务器进入CLOSE_WAIT状态。客户机收到ACK信号后,进入FIN_WAIT_2状态。这时候关闭操作的前半部分已经完成(这个时候服务器还可以继续处理客户机之前发送上来的数据,然后返回给客户机,四次断开就给服务器留下了一个处理尾部数据的时间,当服务器数据全部处理完了,并且也返回给客户机了,这个时候服务器就同样可以发送FIN信号了。这个完全符合我们生活之中实际的例子)。

4、服务器结束最后工作之后,也发送FIN信号。并进入LASK_ACK状态。这时候客户机收到FIN并进行响应,连接完全终止。客户机进入FIN_WAIT状态,服务器进入CLOSE状态。为什么需要FIN_WAIT状态??前面已经说的很清楚。对应服务器子进程僵死。因为父进程没有waitpid操作处理子进程。



处理僵死服务器子进程

捕获信号和waitpid处理僵死进程。在服务器端直接捕获这个信号就可以了,这样僵死进程就不会发生了,很简单的。

void sig_child(int signo)
{
pid_t pid;
while((pid = waitpid(-1 , NULL , WNOHANG)) > 0)//处理子进程,-1表示等待任何子进程,NULL不存取状态,不阻塞。收到信号处理完毕,立即返回,等待捕获下一个信号。
//这样就不会出现僵死进程
printf("child %d terminated\n" , pid);
}


服务器端代码就增加了一个signal注册函数:

void server_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while((n = read(sockfd , buf , MAXLINE)) > 0){
Write(sockfd , buf , n);
}
if(n < 0 && errno == EINTR)//错误或者还有数据,重复读取
goto again;
else if(n < 0)
err_sys("server_echo: read error");
}
void sig_child(int signo)
{
pid_t pid;
while((pid = waitpid(-1 , NULL , WNOHANG)) > 0)//处理子进程,-1表示等待任何子进程,NULL不存取状态,不阻塞。收到信号处理完毕,立即返回,等待捕获信号。
//这样就不会出现僵死进程
printf("child %d terminated\n" , pid);
}
int main(int argc, char **argv)//服务器程序
{
int                 listenfd, connfd;//监听fd和连接fd
struct sockaddr_in  servaddr , cliaddr;
socklen_t clilen;
pid_t childpid;

/*×××××××××××××××××××服务器端套路开始××××××××××××××××××××*/
listenfd = Socket(AF_INET, SOCK_STREAM, 0);//套接字创建
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family      = AF_INET;//服务器IP地址为网卡IP地址
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port        = htons(4099);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));//绑定本机IP地址和端口,设置结构体成员而已
Listen(listenfd, LISTENQ);//绑定之后就可以开始监听外部连接了,指定了排队最大客户连接数
/********************服务器端套路结束*********************/
Signal(SIGCHLD , sig_child);//注册回调函数
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd , (SA *)&cliaddr , &clilen);//传入长度为了达到值-结果
if((childpid = Fork()) == 0){
Close(listenfd);//文件计数关闭一次
server_echo(connfd);
Close(connfd);
exit(0);
}//child,并行服务器就是不停fork处理
Close(connfd);//等待下一个客户机连接。
}
}


异常分析结合状态转换图

通过select poll修改程序

内核一旦发现进程指定的一个或多个IO条件就绪(可读可写),它可以通知进程,这就是IO复用,select和poll支持功能。通过select就可以知道服务器终止了。就返回了

select



void str_cli(FILE *fp , int sockfd)//通过select机制,使得服务器终止服务器便知道,防止阻塞在fgets上
{
char sendline[MAXLINE] , recvline[MAXLINE];
fd_set rset;//定义读描述符集
int maxfdp1;//记录最大描述符加1
FD_ZERO(&rset);//将描述符集清空
for( ; ; ){
FD_SET(fileno(fp) , &rset);//通过文件描述符设置描述符集对于位
FD_SET(sockfd , &rset);
maxfdp1 = max(fileno(fp) , sockfd) + 1;//最大描述符+1
select(maxfdp1 , &rset , NULL , NULL , NULL);//设置测试的读描述符集,写和异常不设置,无限等待直到描述符就绪。
if(FD_ISSET(sockfd , &rset) ){//sockfd可读(rst FIN 数据就绪)
if(Readline(sockfd , recvline , MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
fputs(recvline , stdout);//正常数据,则写到标准输出
}
if(FD_ISSET(fileno(fp) , &rset)){//标准输入可读,读出来然后写入
if(fgets(sendline , MAXLINE , fp) == NULL)
return ;
Write(sockfd , sendline , strlen(sendline));
}
}
}


poll

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: