标准输入 --->fgets--->TCP客户程序------>write------->read--->TCP服务器编写一个完成的echo 程序,来讲解TCP客户/服务器的编写流程。本章的TCP客户/服务器模型:
标准输出 <---fputs<---TCP客户程序<-----read <------write<----TCP服务器
在使用以下内核版本的系统中编译通过:Linux version 2.6.38-8-generic (buildd@vernadsky) (gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu3) )
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define SERV_PORT 5508
#define LISTENQ 10
int
main(int argc, char **argv)
{
int listenfd,connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
memset( (struct sockaddr *)&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if( (listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1){
printf("create socket failure");
exit(1);
}
if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) == -1){
printf("call bind failure");
exit(1);
}
if(listen(listenfd,LISTENQ) == -1){
printf("call listen failure");
exit(1);
}
for(;;){
clilen = sizeof(cliaddr);
if( (connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&clilen)) == -1){
printf("call accept failure");
}
if( (childpid = fork()) == 0){
close(listenfd);
str_echo(connfd);/**process the request**/
exit(0);
}
close(connfd);
}
}
5.3 TCP 的echo 服务器程序的 str_echo 函数
str_echo 具体处理每个客户的请求。读取客户发送的数据并echo给客户。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define SERV_PORT 5508
#define LISTENQ 10
#define MAXLINE 1024
void
str_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){
printf("read error");
exit(0);
}
}
5.4 TCP 的 echo 客户程序的main函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define SERV_PORT 5508
#define LISTENQ 10
#define MAXLINE 1024
int
main(int argc, char **argv)
{
if(argc!=2){
printf("Parameter error !!");
exit(1);
}
int sockfd;
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
sockfd = socket(AF_INET,SOCK_STREAM,0);
connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
str_cli(stdin,sockfd);
exit(0);
}
5.5 TCP的echo客户程序的str_cli 函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define SERV_PORT 5508
#define LISTENQ 10
#define MAXLINE 1024
/**
*没有添加 write 和 read 的错误处理*
**/
void str_cli(FILE *fp,int sockfd)
{
char SendLine[MAXLINE],RecvLine[MAXLINE];
char *ptr;
size_t retsz,wtsz,rdsz;
while(fgets(SendLine,MAXLINE,fp)!=NULL){
wtsz = rdsz = strlen(SendLine);
ptr = SendLine;
while(wtsz > 0){
retsz = write(sockfd,ptr,wtsz);
wtsz -= retsz;
ptr += retsz;
}
ptr = RecvLine;
while(rdsz > 0){
retsz = read(sockfd,ptr,rdsz);
rdsz -= retsz;
ptr += retsz;
}
*ptr = '\0';
fputs(RecvLine,stdout);
}
}
5.6 5.7 略过;
5.8 POSIX 信号处理
(1)信号(signal)也称为软件中断(software interrupt),就是告知某个进程发生了某个事件的通知。
(2)信号通常是异步发生的,也就是說进程预先不知道信号发生的准确时刻。
(3)信号可以由一个进程发给自身或另一个进程,也可以由内核发给某个进程。(4)每一个信号都有一个与之关联的处置(disposition)也称为行为(action).通过调用sigaction来设定一个信号的的行为,并有三种选择。(5)sigaction设置信号行为的三种选择:
((1))调用sigaction设置一个函数,当指定的信号发生时,就调用这个函数,这样的函数称为信号处理函数(signal handler).这种行为称为捕获(catching).有两个信号不能被捕获,分别是:SIGKILL,SIGSTOP 。信号处理函数(signal handler)的原型:void handler(int signo);大多数信号只要求我们调用sigaction并设置信号处理函数。但像 SIGIO,SIGPOLL,SIGURG等等一些信号还需要捕获它的进程做些额外的工作。((2))可以把某个信号的处置设置为SIG_IGN(ignore)来忽略它。SIGKILL 和SIGSTOP这两个信号不能被忽略。((3))可以把某个信号的处置设置为SIG_DFL(default)来启用它的默认处置。默认处置通常是在收到信号后终止进程。但有一些进程的默认处置是忽略。(6)signal 函数
(1)建立信号处置的POSIX方法是调用sigaction函数。也可以调用signal函数,但是signal函数不是POSIX标准函数,并且是不可移植的。
(2)signal 函数的第一个参数是信号名,第二个参数或为指向函数的指针或为常值SIG_IGN或SIG_DFL.
(3)sigaction 函数的简化调用,自定义一个signal函数,在这个函数中调用 sigaction函数,从而达到简化调用的目的。自定义signal函数的代码如下:
(4)信号处理函数一旦安装,便一直安装着。#include <signal.h> typedef void Sigfunc(int); /** *自定义signal函数,简单的对sigaction函数进行封装。 **/ Sigfunc * signal(int signo,Sigfunc *func) { struct sigaction act,oact; act.sa_handler = func;//sigaction的信号处理函数 sigemptyset(&act.sa_mask);//调用信号处理函数期间将被阻塞的信号集 act.sa_flags = 0; if(sigaction(signo,&act,&oact)<0) return (NULL); else return (oact.sa_handler); }
(5)信号处理函数运行期间,被递交的信号是阻塞的,并且sa_mask中设置的所有信号也将被阻塞。
(6)如果一个信号被阻塞期间产生了多次,那么信号解阻塞后通常只递交一次。UNIX的信号默认不排队。
(7)sigprocmask函数可以选择性的阻塞或者解阻塞一组信号。
5.9 处理SIGCHLD信号
(1)设置僵死(zombie)状态的目的是维护子进程的信息。以便父进程在以后莫个时刻获取子进程的进程ID,终止状态以及资源利用信息。
(2)如果一个进程终止,而该进程有子进程正处于僵死状态,那么所有僵死子进程的父进程ID将被重置为1(init进程),init进程将清理这些僵死进程。
(3)如果fork子进程,那么就要wait它们,以防止它们变成僵死进程。
(4)捕获SIGCHLD信号,并在信号处理函数中wait子进程,我们始终应该调用waitpid而非wait来处理子进程。
(5)我们始终应该检查慢系统调用是否返回EINTR错误。并决定是否重启这些系统调用。(一些系统会自动重启被中断的系统调用)。
(6)connect不能被重启,当connect函数被信号中断且不自动重启时,我们必须调用select来等待连接完成。
5.10 wait和waitpid函数
#include <sys/wait.h>
pid_t wait(int *static);
pid_t waitpid(pid_t pid,int *static ,int options);
返回值:成功返回进程ID,出错返回返回0或-1;
参数:int *static ,wait 和waitpid返回时会将子进程的终止状态(一个整数)存放在static中。
pit_t pid 想等待的进程ID号。-1表示等待第一个结束的子进程。
int options 附加选项,常用的是WNOHANG,告知内核在没有以终止子进程时不要阻塞。
wait和waitpid的区别: wait 等待第一个结束的子进程,如果没有结束的子进程,wait将阻塞。waitpid 通过参数设置,可以在没有子进程结束时waitpid不阻塞。
5.11 accept返回前连接终止
Berkeley 的实现在内核中处理终止的连接。POSIX 规定返回一个ECONNABORTED 的 errno.
5.12 服务器进程终止
如果向一个服务进程已终止的服务器发起连接,服务器将返回一个RST 信号。
5.13 SIGPIPE 信号
向一个接收到FIN的套接字写数据会收到RST,
向一个已接收到RST的套接字写数据将引发SIGPIPE信号。并且写操作返回EPIPE错误。
SIGPIPE信号的默认行为是终止进程。
5.14 服务器主机崩溃
如果服务器主机崩溃没有对客户做出响应,将返回ETIMEOUT错误。
如果中间路由器检测到服务器主机不可达,将响应destination unreachable的ICMP消息,内核将返回EHOSTUNREACH错误。
5.15 服务器主机崩溃重启
当服务器主机崩溃重启后,它的所有连接都已经丢失,因此服务器TCP对所收到的来自客户的数据分节响应一个RST。
5.16 服务器主机关机
unix系统关机时,init进程通常先给所有进程发送SIGTERM信号。等待5-20秒后给所有仍然在运行的进程发送SIGKILL信号,这么做的目的是给进程一小段时间来清除和终止。
5.17 TCP程序例子小结
需要通信的客户/服务器程序在通信之前都要指定套接字对。【本地IP地址,本地端口号,外地IP地址,外地端口】。
客户程序的本地IP地址和本地端口号通常是内核分配。服务程序的本地IP地址和端口号有bind函数指定。
5.18 数据格式
网络传递数据遇到的一些问题:
(1)不同的实现以不同的格式存储二进制数,最常见的是大端字节序和小端字节序。
(2)不同的实现在存储相同的C数据类型上可能存在差异,例如32位系统中的long 为32位,64位系统中的long为64位。
(3)不同的实现给结构打包的方式存在差异,取决于各种数据类型所用的位数以及机器的对齐限制,因此,穿越套接字传送二进制结构绝不明智。
解决上述问题的两个常用方法:
(1)把所有的数值数据作为文本串来传递,前提是客户和服务器机器具有相同的字符集。
(2)显式定义所支持数据类型的二进制格式(位数,大端或小端字节序),并以这样的格式在客户与服务器之间传递所有数据。
5.19 小结
这一章通过一个客户/服务器程序展示了在编写网络程序要遇到的问题,包括 信号捕获,处理僵死进程,服务器主机发生错误的几种情况。还讲解了在客户和服务器之间传送的数据的格式。