1.TCP 11種狀態,連接建立三次握手,連接終止四次握手
- 還有一種狀態是closing:產生該狀態的原因比較特殊
- connect打開的是主動套接口,用於發起連接,listen打開的是被動套接口,此套接口只能用於接受連接
- SYN段,ACK段
- ESTABLISH:將未連接隊列的一個條目移動至已連接隊列中,accept從已連接隊列的隊頭返回第一個連接
- 雙方都可以發起關閉
- TIME_WAIT時間:2MSL:TCP段的最大生存期的2倍時間,爲什麼要保留2倍的時間?因爲最後一個ACK不能保證對方能接收,有這個時間可以保證能夠重傳ACK
服務端處於CLOSED的狀態不代表客戶端也處於CLOSED的狀態,所以客戶端要在2MSL的時間後,纔會出現CLOSED的狀態
- closing狀態:雙方同時關閉
一旦收到對方ACK,就會處於TIME_WAIT(TIME_WAIT是由CLOSE發起的一方產生的狀態),但是兩方都處於TIME_WAIT的狀態
2.TIME_WAIT與SO_REUSEADDR
- 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;
}
- eg: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;
}
-
測試1:模擬服務端待用CLOSE
(1)第2個是:客戶端的連接狀態(應該出現在客戶端主機上面,但是這是在同一臺機器上)
(2)第3個是:服務端的連接狀態
模擬服務端調用CLOSE:kill掉與客戶端通信的進程,相當於向客戶端發送了一個FIN的TCP段
(3)爲什麼服務端是FIN_WAIT2狀態,而不是TIME_WAIT狀態,是因爲客戶端read沒有返回0
因爲客戶端的代碼阻塞在while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) ,沒有機會調用readline來返回0,也就意味着客戶端也沒有機會去調用close,也就沒有機會發送FIN段給服務端,所以服務器端服務進入TIME_WAIT狀態,所以保留在FIN_WAIT2狀態
(4)敲一個字符,按下回車,服務端的FIN_WAIT2和客戶端的CLOSE_WAIT的狀態都消失了。
因爲輸入a,會導致客戶端代碼while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) 中fgets返回,從而有機會調用readline,此時就能將服務端發來的FIN段進行接收,read就返回爲0,這裏是readline返回0,則會最終調用close,發送FIN段給服務端,但是此時服務端不存在TIME_WAIT狀態 -
測試2:模擬客戶端調用close
(1)模擬客戶端調用close
在客戶端輸入ctrl b,ctrl b會導致fgets返回爲0
(2)下面是客戶端的TIME_WAIT狀態
(3)客戶端發起close請求,服務器端收到等於0,除了進行確認外,還會發起close ,客戶端收到服務器端發送的FIN,就會處於TIME_WAIT狀態
若服務器端處於TIME_WAIT狀態,即保留2MSL的時間,會導致服務器端無法重新啓動,所以一般要使用REUSEADDR
3.SIGPIPE
- 往一個已經接收FIN的套接字中寫是允許的,接收到FIN僅僅代表對方不再發送數據了。
若發送數據給對方,然而對方進程不存在了,會導致TCP重置。導致對方會發送RST段(含義:連接重置)給我們 - 在收到RST段之後,如果再調用write就會產生SIGPIPE信號,對於這個信號的處理,我們通過忽略即可。
//一般忽略SIGPIPE信號即可
signale(SIGPIPE, SIG_IGN);
- 測試代碼:客戶端: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>
#include <signal.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;
}
//測試收到SIGPIPE信號
void handle_sigpipe(int sig)
{
printf("recv a sig = %d\n", sig);
}
void echo_cli(int sock)
{
//測試收到SIGPIPE信號
signale(SIGPIPE, handle_sigpipe);
//一般忽略SIGPIPE信號即可
signale(SIGPIPE, SIG_IGN);
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)); // 寫入服務器
/*模擬SIGPIPE
若服務端已經關閉了,客戶端收到了FIN,客戶端調用第一個writen,會導致服務端發送一個RST段
過來,再次調用第二個writen,會導致SIGPIPE信號的產生,此信號會終止當前進程,所以不會走
下面的readline
*/
writen(sockfd, sendbuf, 1);//首先發送一個字節
writen(sockfd, sendbuf+1, strlen(sendbuf) - 1);//接着發送剩餘字節
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;
}
-
測試1:
此時處於連接的狀態
關閉通信進程
客戶端:隨便敲一行,然後回車,但是沒有輸出server close?
這是因爲由於客戶端捕捉到了一個SIGPIPE信號,若沒有捕捉到SIGPIPE信號,客戶端的readline應該返回爲0,並輸出server close
-
測試2:
爲什麼能輸出server close?
因爲客戶端捕捉到了SIGPIPE信號,並沒有終止進程,所以客戶端繼續調用readline,最終調用close -
在管道中如何產生SIGPIPE信號?
如果沒有任何讀端進程,然後我們往管道中寫入數據,此時就會出現斷開的管道。
可以把TCP看成是一個全雙工的管道,當某一端收到FIN後,並不能確定對等方的進程已經消失了,因爲對方調用close並不意味着對方的進程會退出,此時客戶端調用write,當他發現對等方的進程不存在了(也可以看成讀端進程不存在了),此時對等方的TCP協議棧會發送一個RST段,再次調用write,會導致SIGPIPE信號的產生