1.TCP回射客戶/服務器
-
TCP客戶/服務器程序
(1)TCP是基於流的,消息與消息之間是沒有邊界的,我們不能假定一次讀操作就返回了整個消息。
所以可以用\n來區分消息之間的邊界,每一行都有一個\n字符,所以就封裝了readline方法來按行讀取。 -
發送不能保證一次調用write方法就將 tcp應用層的所有緩衝區拷貝至套接口緩衝區,所以封裝了writen方法了,進行了更可靠的發送
-
eg:NetworkProgramming-master (1)\LinuxNetworkProgramming\P11echo_srv.c
//
// Created by wangji on 19-8-6.
//
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
//消息通過鍵盤輸出,消息之間的邊界就是/n,就不需要下面的結構體
// struct packet
// {
// int len;
// char buf[1024];
// };
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;
char *bufp = (char*) buf;
while (nleft > 0)
{
nread = read(fd, bufp, nleft);
if (nread < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
} else if (nread == 0)
{
return count - nleft;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char* bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
// recv有數據就返回,沒有數據就阻塞
//若對方套接口關閉,則返回爲0
//recv只能用於套接口
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == -1 && errno == EINTR)//EINTR表示被信號中斷
{
continue;
}
return ret;
}
}
//readline只能用於套接口,因爲使用了recv_peek函數
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = (char*)buf; // 當前指針位置
int nleft = maxline;//maxline一行最大的字節數,但是讀取到\n就可以返回
while (1)
{
ret = recv_peek(sockfd, bufp, nleft);//這裏只是偷窺了緩衝區的數據,但是沒有移走
if (ret < 0)
{
return ret;
}
else if (ret == 0)//ret == 0表示對方關閉套接口
{
return ret;
}
nread = ret;
//判斷接收緩衝區是否有\n
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n')//若有\n,則將其作爲一條消息讀走
{
ret = readn(sockfd, bufp, i+1);//將數據從緩衝區移除,讀取到i,說明有i+1個數據,包括\n
if (ret != i+1)//接收到的字節數不等於i+1,說明失敗
{
exit(EXIT_FAILURE);
}
return ret;//返回一條消息
}
}
// 若沒有\n,說明還不滿一條消息,也需要將數據讀出來,放到緩衝區bufp
if (nread > nleft)//從緩衝區讀到的字節數要小於剩餘字節數,否則有問題
{
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
{
exit(EXIT_FAILURE);
}
bufp += nread;//指針偏移,將數據放到屁股後面
}
return -1;
}
void echo_srv(int connfd)
{
char recvbuf[1024];
int n;
while (1)
{
memset(recvbuf, 0, sizeof recvbuf);
int ret = readline(connfd, recvbuf, 1024);//按行接收到緩衝區
if (ret == -1)
{
ERR_EXIT("readline");
}
if (ret == 0)
{
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
writen(connfd, recvbuf, strlen(recvbuf));
}
}
// void handle_sigchld(int sig)
// {
// // wait(NULL);//捕獲子進程的退出狀態。man 2 wait,NULL:這裏退出狀態不關心
// // waitpid(-1, NULL, WNOHANG);//可以等待所有子進程,WNOHANG表示不掛起
// //輪詢子進程的退出狀態
// while (waitpid(-1, NULL, WNOHANG) > 0 )//將所有子進程的退出狀態進行返回, >0表示等待到了一個子進程
// ;//由於指定WNOHANG,則沒有子進程退出則返回-1,退出while
// }
int main(int argc, char** argv) {
// signal(SIGCHLD , SIG_IGN);//SIGCHLD可以忽略殭屍進程,不建議採用
// signal(SIGCHLD, handle_sigchld);
// 1. 創建套接字
int listenfd;
if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// 2. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
int on = 1;
// 確保time_wait狀態下同一端口仍可使用
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0) {
ERR_EXIT("setsockopt");
}
// 3. 綁定套接字地址
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
ERR_EXIT("bind");
}
// 4. 等待連接請求狀態
if (listen(listenfd, SOMAXCONN) < 0) {
ERR_EXIT("listen");
}
// 5. 允許連接
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof peeraddr;
// 6. 數據交換
pid_t pid;
while (1) {
int connfd;
if ((connfd = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen)) < 0) {
ERR_EXIT("accept");
}
printf("id = %s, ", inet_ntoa(peeraddr.sin_addr));
printf("port = %d\n", ntohs(peeraddr.sin_port));
pid = fork();
if (pid == -1) {
ERR_EXIT("fork");
}
if (pid == 0) // 子進程
{
close(listenfd);
echo_srv(connfd);
//printf("child exit\n");
exit(EXIT_SUCCESS);
} else {
//printf("parent exit\n");
close(connfd);
}
}
// 7. 斷開連接
close(listenfd);
return 0;
}
- NetworkProgramming-master (1)\LinuxNetworkProgramming\P11echo_cli.c
//
// Created by wangji on 19-8-6.
//
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
struct packet
{
int len;
char buf[1024];
};
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;
char *bufp = (char*) buf;
while (nleft > 0)
{
nread = read(fd, bufp, nleft);
if (nread < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
} else if (nread == 0)
{
return count - nleft;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char* bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
int ret = recv(sockfd, buf, len, MSG_PEEK); // 查看傳入消息
if (ret == -1 && errno == EINTR)
{
continue;
}
return ret;
}
}
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = (char*)buf; // 當前指針位置
int nleft = maxline;
while (1)
{
ret = recv_peek(sockfd, buf, nleft);
if (ret < 0)
{
return ret;
}
else if (ret == 0)
{
return ret;
}
nread = ret;
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+1);
if (ret != i+1)
{
exit(EXIT_FAILURE);
}
return ret;
}
}
if (nread > nleft)
{
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
{
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void echo_cli(int sock)
{
char recvbuf[1024]= [0];
char sendbuf[1024]= [0];
// struct packet recvbuf;
// struct packet sendbuf;
memset(recvbuf, 0, sizeof recvbuf);
memset(sendbuf, 0, sizeof sendbuf);
int n = 0;
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) // 鍵盤輸入獲取,默認帶\n
{
writen(sockfd, sendbuf, strlen(sendbuf)); // 寫入服務器
int ret = readline(sockfd, recvbuf, sizeof(recvbuf)); // 服務器讀取
if (ret == -1)
{
ERR_EXIT("readline");
}
if (ret == 0)
{
printf("server close\n");
break;
}
fputs(recvbuf, stdout); // 服務器返回數據輸出
// 清空
memset(recvbuf, 0, sizeof(recvbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
}
int main(int argc, char** argv) {
// 1. 創建套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// 2. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
// servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
// 3. 請求鏈接
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
ERR_EXIT("connect");
}
struct sockaddr_in localaddr;//本地地址
socklen_t addrlen = sizeof(localaddr);//要有初始值,和accept是一樣的
//已連接的套接口sockfd,既有本地地址,又有對等方的地址
if (getsockname(sockfd, (struct sockaddr*)&localaddr, &addrlen) < 0)
{
ERR_EXIT("getsockname");
}
printf("id = %s, ", inet_ntoa(localaddr.sin_addr));
printf("port = %d\n", ntohs(localaddr.sin_port));
// 4. 數據交換
echo_cli(sockfd);
// 5. 斷開連接
close(sockfd);
return 0;
}
- 測試:
2.TCP是個流協議
- TCP是基於字節流傳輸的,只維護髮送出去多少,確認了多少,沒有維護消息與消息之間的邊界,因而可能導致粘包問題
- 粘包問題解決方案是在應用層維護消息邊界
3.僵進程與SIGCHLD信號
- (1)方法1,不建議採用
signal(SIGCHLD , SIG_IGN);
忽略SIGCHLD信號;
- (2)方法2:
signal(SIGCHLD, handle_sigchld);
捕捉SIGCHLD信號,來避免殭屍進程
- 方法1的eg:NetworkProgramming-master (1)\LinuxNetworkProgramming\P11echo_srv.c
//
// Created by wangji on 19-8-6.
//
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
//消息通過鍵盤輸出,消息之間的邊界就是/n,就不需要下面的結構體
// struct packet
// {
// int len;
// char buf[1024];
// };
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;
char *bufp = (char*) buf;
while (nleft > 0)
{
nread = read(fd, bufp, nleft);
if (nread < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
} else if (nread == 0)
{
return count - nleft;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char* bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
// recv有數據就返回,沒有數據就阻塞
//若對方套接口關閉,則返回爲0
//recv只能用於套接口
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == -1 && errno == EINTR)//EINTR表示被信號中斷
{
continue;
}
return ret;
}
}
//readline只能用於套接口,因爲使用了recv_peek函數
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = (char*)buf; // 當前指針位置
int nleft = maxline;//maxline一行最大的字節數,但是讀取到\n就可以返回
while (1)
{
ret = recv_peek(sockfd, bufp, nleft);//這裏只是偷窺了緩衝區的數據,但是沒有移走
if (ret < 0)
{
return ret;
}
else if (ret == 0)//ret == 0表示對方關閉套接口
{
return ret;
}
nread = ret;
//判斷接收緩衝區是否有\n
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n')//若有\n,則將其作爲一條消息讀走
{
ret = readn(sockfd, bufp, i+1);//將數據從緩衝區移除,讀取到i,說明有i+1個數據,包括\n
if (ret != i+1)//接收到的字節數不等於i+1,說明失敗
{
exit(EXIT_FAILURE);
}
return ret;//返回一條消息
}
}
// 若沒有\n,說明還不滿一條消息,也需要將數據讀出來,放到緩衝區bufp
if (nread > nleft)//從緩衝區讀到的字節數要小於剩餘字節數,否則有問題
{
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
{
exit(EXIT_FAILURE);
}
bufp += nread;//指針偏移,將數據放到屁股後面
}
return -1;
}
void echo_srv(int connfd)
{
char recvbuf[1024];
int n;
while (1)
{
memset(recvbuf, 0, sizeof recvbuf);
int ret = readline(connfd, recvbuf, 1024);//按行接收到緩衝區
if (ret == -1)
{
ERR_EXIT("readline");
}
if (ret == 0)
{
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
writen(connfd, recvbuf, strlen(recvbuf));
}
}
void handle_sigchld(int sig)
{
wait(NULL);//捕獲子進程的退出狀態。man 2 wait,NULL:這裏退出狀態不關心
// waitpid(-1, NULL, WNOHANG);//可以等待所有子進程,WNOHANG表示不掛起
//輪詢子進程的退出狀態
// while (waitpid(-1, NULL, WNOHANG) > 0 )//將所有子進程的退出狀態進行返回, >0表示等待到了一個子進程
;//由於指定WNOHANG,則沒有子進程退出則返回-1,退出while
}
int main(int argc, char** argv) {
signal(SIGCHLD , SIG_IGN);//SIGCHLD可以忽略殭屍進程,不建議採用
// signal(SIGCHLD, handle_sigchld);
// 1. 創建套接字
int listenfd;
if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// 2. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
int on = 1;
// 確保time_wait狀態下同一端口仍可使用
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0) {
ERR_EXIT("setsockopt");
}
// 3. 綁定套接字地址
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
ERR_EXIT("bind");
}
// 4. 等待連接請求狀態
if (listen(listenfd, SOMAXCONN) < 0) {
ERR_EXIT("listen");
}
// 5. 允許連接
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof peeraddr;
// 6. 數據交換
pid_t pid;
while (1) {
int connfd;
if ((connfd = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen)) < 0) {
ERR_EXIT("accept");
}
printf("id = %s, ", inet_ntoa(peeraddr.sin_addr));
printf("port = %d\n", ntohs(peeraddr.sin_port));
pid = fork();
if (pid == -1) {
ERR_EXIT("fork");
}
if (pid == 0) // 子進程
{
close(listenfd);
echo_srv(connfd);
//printf("child exit\n");
exit(EXIT_SUCCESS);
} else {
//printf("parent exit\n");
close(connfd);
}
}
// 7. 斷開連接
close(listenfd);
return 0;
}
-
客戶端代碼同:NetworkProgramming-master (1)\LinuxNetworkProgramming\P11echo_cli.c
-
測試:將客戶端關閉,服務端就沒有殭屍進程了
-
若服務端的多個子進程同時退出,則wait僅僅等待第一個子進程退出就返回了,此時需要waitpid
有5個客戶端連接服務器,同時退出的情況
-
eg:5個客戶端的代碼:NetworkProgramming-master (1)\LinuxNetworkProgramming\P11echocli5.cpp
//
// Created by wangji on 19-8-6.
//
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
struct packet
{
int len;
char buf[1024];
};
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;
char *bufp = (char*) buf;
while (nleft > 0)
{
nread = read(fd, bufp, nleft);
if (nread < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
} else if (nread == 0)
{
return count - nleft;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char* bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
int ret = recv(sockfd, buf, len, MSG_PEEK); // 查看傳入消息
if (ret == -1 && errno == EINTR)
{
continue;
}
return ret;
}
}
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = (char*)buf; // 當前指針位置
int nleft = maxline;
while (1)
{
ret = recv_peek(sockfd, buf, nleft);
if (ret < 0)
{
return ret;
}
else if (ret == 0)
{
return ret;
}
nread = ret;
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+1);
if (ret != i+1)
{
exit(EXIT_FAILURE);
}
return ret;
}
}
if (nread > nleft)
{
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
{
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void ehco_cli(int sockfd)
{
char recvbuf[1024];
char sendbuf[1024];
// struct packet recvbuf;
// struct packet sendbuf;
memset(recvbuf, 0, sizeof recvbuf);
memset(sendbuf, 0, sizeof sendbuf);
int n = 0;
while (fgets(sendbuf, sizeof sendbuf, stdin) != NULL) // 鍵盤輸入獲取
{
writen(sockfd, sendbuf, strlen(sendbuf)); // 寫入服務器
int ret = readline(sockfd, recvbuf, sizeof recvbuf); // 服務器讀取
if (ret == -1)
{
ERR_EXIT("readline");
}
if (ret == 0)
{
printf("server close\n");
break;
}
fputs(recvbuf, stdout); // 服務器返回數據輸出
// 清空
memset(recvbuf, 0, sizeof recvbuf);
memset(sendbuf, 0, sizeof sendbuf);
}
}
int main(int argc, char** argv) {
// 1. 創建套接字
int sockfd[5];
int i;
for (i = 0; i < 5; ++i)
{
if ((sockfd[i] = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// 2. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
// servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
// 3. 請求鏈接
if (connect(sockfd[i], (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
ERR_EXIT("connect");
}
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof localaddr;
if (getsockname(sockfd[i], (struct sockaddr*)&localaddr, &addrlen) < 0)
{
ERR_EXIT("getsockname");
}
printf("id = %s, ", inet_ntoa(localaddr.sin_addr));
printf("port = %d\n", ntohs(localaddr.sin_port));
}
// 4. 數據交換
ehco_cli(sockfd[0]);
// 5. 斷開連接
close(sockfd[0]);
return 0;
}
- eg:服務端代碼
//
// Created by wangji on 19-8-6.
//
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
//消息通過鍵盤輸出,消息之間的邊界就是/n,就不需要下面的結構體
// struct packet
// {
// int len;
// char buf[1024];
// };
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;
char *bufp = (char*) buf;
while (nleft > 0)
{
nread = read(fd, bufp, nleft);
if (nread < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
} else if (nread == 0)
{
return count - nleft;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char* bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
// recv有數據就返回,沒有數據就阻塞
//若對方套接口關閉,則返回爲0
//recv只能用於套接口
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == -1 && errno == EINTR)//EINTR表示被信號中斷
{
continue;
}
return ret;
}
}
//readline只能用於套接口,因爲使用了recv_peek函數
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = (char*)buf; // 當前指針位置
int nleft = maxline;//maxline一行最大的字節數,但是讀取到\n就可以返回
while (1)
{
ret = recv_peek(sockfd, bufp, nleft);//這裏只是偷窺了緩衝區的數據,但是沒有移走
if (ret < 0)
{
return ret;
}
else if (ret == 0)//ret == 0表示對方關閉套接口
{
return ret;
}
nread = ret;
//判斷接收緩衝區是否有\n
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n')//若有\n,則將其作爲一條消息讀走
{
ret = readn(sockfd, bufp, i+1);//將數據從緩衝區移除,讀取到i,說明有i+1個數據,包括\n
if (ret != i+1)//接收到的字節數不等於i+1,說明失敗
{
exit(EXIT_FAILURE);
}
return ret;//返回一條消息
}
}
// 若沒有\n,說明還不滿一條消息,也需要將數據讀出來,放到緩衝區bufp
if (nread > nleft)//從緩衝區讀到的字節數要小於剩餘字節數,否則有問題
{
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
{
exit(EXIT_FAILURE);
}
bufp += nread;//指針偏移,將數據放到屁股後面
}
return -1;
}
void echo_srv(int connfd)
{
char recvbuf[1024];
int n;
while (1)
{
memset(recvbuf, 0, sizeof recvbuf);
int ret = readline(connfd, recvbuf, 1024);//按行接收到緩衝區
if (ret == -1)
{
ERR_EXIT("readline");
}
if (ret == 0)
{
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
writen(connfd, recvbuf, strlen(recvbuf));
}
}
void handle_sigchld(int sig)
{
wait(NULL);//捕獲子進程的退出狀態。man 2 wait,NULL:這裏退出狀態不關心
// waitpid(-1, NULL, WNOHANG);//可以等待所有子進程,WNOHANG表示不掛起
//輪詢子進程的退出狀態
// while (waitpid(-1, NULL, WNOHANG) > 0 )//將所有子進程的退出狀態進行返回, >0表示等待到了一個子進程
;//由於指定WNOHANG,則沒有子進程退出則返回-1,退出while
}
int main(int argc, char** argv) {
signal(SIGCHLD , SIG_IGN);//SIGCHLD可以忽略殭屍進程,不建議採用
// signal(SIGCHLD, handle_sigchld);
// 1. 創建套接字
int listenfd;
if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// 2. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
int on = 1;
// 確保time_wait狀態下同一端口仍可使用
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0) {
ERR_EXIT("setsockopt");
}
// 3. 綁定套接字地址
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
ERR_EXIT("bind");
}
// 4. 等待連接請求狀態
if (listen(listenfd, SOMAXCONN) < 0) {
ERR_EXIT("listen");
}
// 5. 允許連接
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof peeraddr;
// 6. 數據交換
pid_t pid;
while (1) {
int connfd;
if ((connfd = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen)) < 0) {
ERR_EXIT("accept");
}
printf("id = %s, ", inet_ntoa(peeraddr.sin_addr));
printf("port = %d\n", ntohs(peeraddr.sin_port));
pid = fork();
if (pid == -1) {
ERR_EXIT("fork");
}
if (pid == 0) // 子進程
{
close(listenfd);
echo_srv(connfd);
//printf("child exit\n");
exit(EXIT_SUCCESS);
} else {
//printf("parent exit\n");
close(connfd);
}
}
// 7. 斷開連接
close(listenfd);
return 0;
}
-
測試:
退出一個客戶端,但是有4個服務端子進程處於殭屍狀態。因爲服務端程序的wait只能返回一個子進程
-
若不加while循環直接使用waitpid。5個客戶端代碼不變,服務端需要修改的代碼如下:
void handle_sigchld(int sig)
{
// wait(NULL);//捕獲子進程的退出狀態。man 2 wait,NULL:這裏退出狀態不關心
waitpid(-1, NULL, WNOHANG);//可以等待所有子進程,WNOHANG表示不掛起
//輪詢子進程的退出狀態
// while (waitpid(-1, NULL, WNOHANG) > 0 )//將所有子進程的退出狀態進行返回, >0表示等待到了一個子進程
;//由於指定WNOHANG,則沒有子進程退出則返回-1,退出while
}
int main(int argc, char** argv) {
// signal(SIGCHLD , SIG_IGN);//SIGCHLD可以忽略殭屍進程,不建議採用
signal(SIGCHLD, handle_sigchld);
- 結果:
- 原因解釋如下:
關閉客戶端後,服務器段出現了3個或者4個殭屍進程的原因。
當客戶端終止時,會向服務器端發送FIN,服務器端收到FIN=0,就會退出子進程,迴向父進程發送SIGCHLD信號,若這些信號同時到達的話,則只可能處理一個,因爲這些不可靠信號不支持排隊的,只處理一個則出現了4個殭屍進程,若不是同時到達,若SIGCHLD被執行2次,那麼就出現了3個殭屍進程
- 5個客戶端不變,服務端修改爲:waitpid加上while循環
void handle_sigchld(int sig)
{
// wait(NULL);//捕獲子進程的退出狀態。man 2 wait,NULL:這裏退出狀態不關心
// waitpid(-1, NULL, WNOHANG);//可以等待所有子進程,WNOHANG表示不掛起
//輪詢子進程的退出狀態
while (waitpid(-1, NULL, WNOHANG) > 0 )//將所有子進程的退出狀態進行返回, >0表示等待到了一個子進程
;//由於指定WNOHANG,則沒有子進程退出則返回-1,退出while
}
int main(int argc, char** argv) {
// signal(SIGCHLD , SIG_IGN);//SIGCHLD可以忽略殭屍進程,不建議採用
signal(SIGCHLD, handle_sigchld);
-
結果:
此時沒有殭屍進程了
-
Makefile文件
.PHONY:clean all
CC=gcc
CFLAGS=-Wall -g
BIN=echosrv echocli echocli2
all:$(BIN)
%.o:%.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f *.o $(BIN)