首先補充一個關於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;
}
可見,不會出現殭屍線程。