首先补充一个关于read函数的点:
如果顺利read()会返回实际读到的字节数,最好能将返回值与参数count作比较,若返回的字节数比要求读取的字节数少,则说明读到了文件尾部。当有错误发生时则返回-1,错误代码存入errno中,而文件读写位置则无法预期。
read 返回值:
1.> 0
实际读到的字节数
2.= 0
数据读完(读到文件、管道、socket 末尾–对端关闭)
3.-1
异常
3.1errno == EINTR
说明此调用被信号中断 重启/quit
3.2errno == EAGAIN (EWOULDBLOCK)
非阻塞方式读即使用不可阻断I/O 时(O_NONBLOCK),若无数据可读取则返回此值。
3.其他值 出现错误。–perror exit。
因此,最好对原有的read/write函数重新包装一下:
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
多进程并发服务器实现
其实多进程并发的重点在于分工:父进程只负责监听,并在connect成功后创建新的子进程。而子进程则只负责通信。最后,对于子进程的回收,采用信号机制,子进程正常结束后会向父进程发送SIGCHLD信号,所以最好的回收办法就是在父进程中注册信号捕捉函数。
server.c
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <ctype.h>
#include <unistd.h>
#include "wrap.h"
#define MAXLINE 8192
#define SERV_PORT 6666
void do_sigchild(int num)
{
while (waitpid(0, NULL, WNOHANG) > 0)
{
}
}
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i;
pid_t pid;
struct sigaction newact;
newact.sa_handler = do_sigchild;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGCHLD, &newact, NULL);
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//允许端口复用(在bind函数前调用)
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, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1)
{
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
pid = fork();
if (pid == 0)//子进程
{
Close(listenfd);
while (1)
{
int n = Read(connfd, buf, MAXLINE);
if (n == 0) //说明客户端关闭通信套接字,那么当前子进程跳出循环并结束(会向父进程发出SIGCHLD信号)
{
printf("the other side has been closed.\n");
break;
}
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
Write(STDOUT_FILENO, buf, n);
Write(connfd, buf, n);
}
Close(connfd);
return 0;
}
else if (pid > 0) //父进程
{
Close(connfd);
}
else
perr_exit("fork");
}
return 0;
}
client.c
/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "wrap.h"
#define MAXLINE 8192
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL)
{
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0) // //说明服务器端关闭了通信套接字
{
printf("the other side has been closed.\n");
break;
}
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
可以看到,子进程成功被回收,并没有成为僵尸进程。
多线程并发服务器实现
多线程的回收只需要detach就可以了。
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include "wrap.h"
#define MAXLINE 8192
#define SERV_PORT 6666
struct s_info
{ //定义一个结构体, 方便直接打包传参给线程函数
struct sockaddr_in cliaddr;
int connfd;
};
void *do_work(void *arg)
{
int n,i;
struct s_info *ts = (struct s_info*)arg;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16 可用"[+d"查看
while (1)
{
n = Read(ts->connfd, buf, MAXLINE); //读客户端
if (n == 0)
{
printf("the client %d closed...\n", ts->connfd);
break; //跳出循环,关闭cfd
}
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
ntohs((*ts).cliaddr.sin_port)); //打印客户端信息(IP/PORT)
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]); //小写-->大写
Write(STDOUT_FILENO, buf, n); //写出至屏幕
Write(ts->connfd, buf, n); //回写给客户端
}
Close(ts->connfd);
return (void *)0;
}
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
pthread_t tid;
struct s_info ts[256]; //根据最大线程数创建结构体数组.
int i = 0;
listenfd = Socket(AF_INET, SOCK_STREAM, 0); //创建一个socket, 得到lfd
bzero(&servaddr, sizeof(servaddr)); //地址结构清零
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //指定本地任意IP
servaddr.sin_port = htons(SERV_PORT); //指定端口号 6666
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //绑定
Listen(listenfd, 128); //设置同一时刻链接服务器上限数
printf("Accepting client connect ...\n");
while (1)
{
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); //阻塞监听客户端链接请求
ts[i].cliaddr = cliaddr;
ts[i].connfd = connfd;
/* 达到线程最大数时,pthread_create出错处理, 增加服务器稳定性 */
pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
pthread_detach(tid); //子线程分离,防止僵线程产生.
i++;
}
return 0;
}
可见,不会出现僵尸线程。