经过半天到努力,终于写好一个采用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);
}
对于采用fork子进程编写的tcp server,有如下几点需要注意
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位机。所以传递二进制是很不明智的。如果非要传递二进制,却需要明确定义二进制格式(每项数据所占用的位数,大端、小端字节序的问题)