经过半天到努力,终于写好一个采用fork子进程方法编写到tcp服务器,直接上代码。
tcp server:
/* socksrv.c*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> /* for struct sockaddr_in*/ #include <sys/errno.h> #include <signal.h> #define SVR_IP "127.0.0.1" #define SVR_PORT 1234 #define BACKLOG 10 #define MAX_BUF_LEN 1024 void chld_handle(int sig) { pid_t pid; int stat; while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) { printf("child %d terminated\n", pid); } return; } int main() { struct sigaction act; act.sa_handler = chld_handle; sigemptyset(&act.sa_mask); act.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &act, 0); int listenfd = 0; int connfd = 0; int ret = 0; struct sockaddr_in svr_addr, cli_addr; memset(&svr_addr, 0, sizeof(struct sockaddr_in)); memset(&cli_addr, 0, sizeof(struct sockaddr_in)); // create listen socket fd listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { perror("socket failed"); exit(1); } // bind svr ip and port svr_addr.sin_family = AF_INET; svr_addr.sin_port = htons(SVR_PORT); svr_addr.sin_addr.s_addr = inet_addr(SVR_IP); ret = bind(listenfd, (struct sockaddr*)&svr_addr, sizeof(struct sockaddr)); if (ret == -1) { perror("bind failed"); exit(1); } // listen client ret = listen(listenfd, BACKLOG); if (ret == -1) { perror("listen failed"); exit(1); } printf("server is on...\n"); while(1) { int sin_size = sizeof(struct sockaddr_in); if((connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &sin_size)) < 0) { perror("accept failed"); if (errno == EINTR) { perror("child process cause it\n"); continue; } else { break; } } pid_t child_pid = fork(); if (0 == child_pid) // this is child process { close(listenfd); printf("one socket(%d) client come\n", connfd); char recv_buf[MAX_BUF_LEN] = {0}; int recv_len = 0; while((recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0) { printf("svr %d recv %s recv_len %d\n", connfd, recv_buf, recv_len); ret = send(connfd, recv_buf, strlen(recv_buf), 0); if (ret < 0) { perror("send failed"); exit(1); } memset(recv_buf, 0, sizeof(recv_buf)); } if (recv_len < 0) { perror("recv failed"); exit(1); } printf("one socket(%d) client go\n", connfd); close(connfd); exit(0); } close(connfd); } close(listenfd); return 0; }
tcp client:
/* sockclnt.c*/ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> /*for struct sockaddr_in*/ #define SVR_IP "127.0.0.1" #define SVR_PORT 1234 #define BUFSIZE 255 int main() { int ret = 0; int sockfd = 0; struct sockaddr_in svr_addr; memset(&svr_addr, 0, sizeof(struct sockaddr_in)); char * msg = "'path:/home/dongshen/media_file/b_1.ts'"; // create client socket fd sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket failed"); exit(1); } // connet to server svr_addr.sin_family = AF_INET; svr_addr.sin_port = htons(SVR_PORT); svr_addr.sin_addr.s_addr = inet_addr(SVR_IP); ret = connect(sockfd, (struct sockaddr *)&svr_addr, sizeof(struct sockaddr_in)); if (ret == -1) { perror("connect failed"); exit(1); } int i = 0; for (i = 0; i < 5; i++) { char * msg = "hello server...."; printf("client send msg: %s\n", msg); if((send(sockfd, msg, strlen(msg), 0))<0) { perror("send()"); } char recv_buf[BUFSIZE] = {0}; int recv_len = recv(sockfd, recv_buf, sizeof(recv_buf), 0); printf("client recv msg: %s%d\n", recv_buf, recv_len); sleep(10); } close(sockfd); }
1. 主进程要关闭子进程的connfd,子进程要关闭主进程到listenfd,原因是,在采用fork方法fork出子进程之后,这俩fd都会在每个进程中有个副本,如果不关闭,则会造成和客户端到连接无法正常关闭。
2. 对于子进程一定要采用sigaction方法截获SIGCHLD信号,并采用waitpid来彻底终止子进程,否则子进程将编程僵尸进程,而且一定要采用waitpid,不能采用wait,因为如果同一时间有多个子进程同时关闭(如多个客户端同时关闭),则会造成大量SIGCHLD信号,由于sigaction会对信号堵塞,同时多个排队信号,会进行合并处理,会造成信号丢失,产生僵尸进程,如果采用waitpid,则会同一时间循环处理掉已经结束的子进程。
3. 在信号SIGCHLD来临之际,主进程正堵塞在accept,中断后,在有些系统,accept不能由内核重启,造成accept返回错误EINTR,所以需要对其特殊处理,continue,手动重启accept,但是在centos系统中,经过测试当将sigaction中的sa_flags设置为SA_RESTART,中断到accept会被内核自动重启,无此顾虑
下面让我们来讨论几种服务器异常情况:
1、服务器进程终止
服务器进程终止,那么在终止时刻,socket fd会正常关闭,也就是说内核会往客户端发送FIN消息。此时
如果客户端进程没有堵塞与read,而是仍然write数据给服务器,则服务器会返回RST消息;
如果客户端连续多次write调用,第一个write引起服务器RST消息,客户端内核收到RST消息后,进程仍然write数据,内核会向进程发送SIGPIPE信号,该信号默认行为为终止进程,同时write会返回EPIPE错误。那么是否捕获该信号,则由于具体业务决定。
2、服务器主机崩溃
如果服务器主机突然崩溃(不是正常关机),这个时候客户端再发送数据,则会产生超时重传,或者返回ETIMEOUT、EHOSTUNREACH、ENETUNREACH错误。
3、服务器主机崩溃并重启
这个时候客户端如果在发送数据给服务端,服务端则会响应一个RST消息。
4、服务器主机关机
如果主机关机,init会想所有进程发送SIGTERM信号,等待5~20s,然后向所有活着的进程发送SIGKILL信号,这样服务进程会想客户端发送FIN消息。
总结:从刚刚这几个服务端的异常可以看出,客户端有几件事很重要:
1、探测服务器是否还活着很重要,SO_KEEPALIVE套接字选项。
2、一旦服务器发送FIN或RST,客户端能及时感知到,通过select和epoll。
传送的数据格式
比如传递两个数字,有下面两种方法。
1、字符串,客户端和服务端分别维护一段buf,传递字符串,a+空格+b,然后到服务端在解析该字符串
2、二进制,定义一个结构体,直接传递结构体变量指针,数据长度为结构体长度
struct
{
long a;
long b;
}
<注意>:如果传递二进制,则需要考虑两点主机字节序的问题,并且如果为long型,还需要考虑是32位机还是64位机。所以传递二进制是很不明智的。如果非要传递二进制,却需要明确定义二进制格式(每项数据所占用的位数,大端、小端字节序的问题)