close終止了數據傳送的兩個方向。
shutdown可以有選擇的終止某個方向的數據傳送或者終止數據傳送的兩個方向。
shutdown how=1就可以保證對等方接收到一個EOF字符,而不管其他進程是否已經打開了套接字。而close不能保證,直到套接字引用計數減爲0時才發送。也就是說直到所有的進程都關閉了套接字。
思考1
客戶端向服務器發送:FIN(close) E D C B A,
問:服務器還能收到數據嗎?服務器還可以向客戶端回報文嗎?
客戶端想在關閉之後,仍然能接收到回射服務器應答(shutdown)。
思考2
父進程中close(conn);會不會向客戶端發送FIN報文段那?
文件的引用計數-1,當減少爲0,纔會發送引用計數。
思考3:
客戶端//shutdown(sock, SHUT_WR);只關閉了寫;
言外之意我可以接收數據
測試close與shutdown的區別
第一種測試場景
運行結果如下
現在調用shutdown
實驗結果
源代碼:
9client_readline.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
/*
包尾加上\n編程實踐
*/
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
/*
使用說明:
//1一次全部讀走 //2次讀完數據 //出錯分析 //對方已關閉
思想:
tcpip是流協議,不能保證1次讀操作,能全部把報文讀走,所以要循環
讀指定長度的數據。
按照count大小讀數據,若讀取的長度ssize_t<count 說明讀到了一個結束符,
對方已關閉
函數功能:
從一個文件描述符中讀取count個字符到buf中
參數:
@buf:接受數據內存首地址
@count:接受數據長度
返回值:
@ssize_t:返回讀的長度 若ssize_t<count 讀失敗失敗
*/
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; //剩下需要讀取的數據個數
ssize_t nread; //成功讀取的字節數
char * bufp = (char*)buf;//將參數接過來
while (nleft > 0)
{
//如果errno被設置爲EINTR爲被信號中斷,如果是被信號中斷繼續,
//不是信號中斷則退出。
if ((nread = read(fd, bufp, nleft)) < 0)
{
//異常情況處理
if (errno == EINTR) //讀數據過程中被信號中斷了
continue; //再次啓動read
//nread = 0; //等價於continue
return -1;
}else if (nread == 0) //到達文件末尾EOF,數據讀完(讀文件、讀管道、socket末尾、對端關閉)
break;
bufp += nread; //將字符串指針向後移動已經成功讀取個數的大小。
nleft -=nread; //需要讀取的個數=需要讀取的個數-已經成功讀取的個數
}
return (count - nleft);//返回已經讀取的數據個數
}
/*
思想:tcpip是流協議,不能1次把指定長度數據,全部寫完
按照count大小寫數據
若讀取的長度ssize_t<count 說明讀到了一個結束符,對方已關閉。
函數功能:
向文件描述符中寫入count個字符
函數參數:
@buf:待寫數據首地址
@count:待寫長度
返回值:
@ssize_t:返回寫的長度 -1失敗
*/
ssize_t writen(int fd, 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 ((nwritten < 0) && (errno == EINTR)) //讀數據過程中被信號中斷了
continue; //再次啓動write
//nwritten = 0; //等價continue
else
return -1;
}
bufp += nwritten; //移動緩衝區指針
nleft -=nwritten; //記錄剩下未讀取的數據
}
return count;//返回已經讀取的數據個數
}
//讀數據,但不把數據緩衝區清空
//@ssize_t返回值:返回緩衝區數據的長度 -1失敗
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
//MSG_PEEK 讀取隊列中指定大小的數據,但不取出
int ret = recv(sockfd, buf, len, MSG_PEEK);
//如果被信號中斷,則繼續
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
/*
maxline 一行最大數
先提前peek一下緩衝區,如果有數據從緩衝區讀數據,
1、緩衝區數據中帶\n
2 、緩存區中不帶\n
讀取數據包直到\n
功能:按行讀取文件,只要遇到\n就,讀走數據,返回,
@buf 接收數據內存首地址
@maxline 接收數據內存最大值
*/
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread; //成功預讀取的數據個數
char *bufp = buf; //讀取數據存放的數組,在外分配內存
int nleft = maxline;//封包最大值
while (1)
{
//看一看緩衝區有沒有數據,並不移除內核緩衝區數據
//讀數據,但不把數據緩衝區清空,成功:ret是報文的長度
ret = recv_peek(sockfd, bufp, nleft);
if (ret < 0) //失敗
return ret;
else if (ret == 0)//對方已關閉
return ret;
nread = ret;
int i;
//讀數據,但不把數據緩衝區清空,避免了一個字節一個字節的讀數據
//先利用recv的MSG_PEEK功能,預讀數據,然後查找\n
//根據\n的位置,根據指定長度,再真正的讀數據
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n') //若緩衝區有\n
{
ret = readn(sockfd, bufp, i+1);//將數據從緩存區讀走
if (ret != i + 1)
exit(EXIT_FAILURE);
return ret;//有\n就返回,並返回讀走的數據
}
}
//若數據長度 nread > 緩衝區最大長度maxline 退出
if (nread > nleft)
exit(EXIT_FAILURE);
//若沒有\n,說明消息還沒有結束,不是完整的一條消息,就把這些數據也讀到buf緩衝區中。
//依此循環,直到遇到\n,把整個一行數據,全部讀完,放入buf中
//bufp記錄了每次需追加的位置
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
exit(EXIT_FAILURE);
bufp += nread; //bufp每次跳到追加的末尾
}
return -1;
}
void echo_cli(int sockfd)
{
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//寫數據(本身帶有\n), 所以不需要再單獨加\n,因爲從stdin輸入完需要按下enter鍵
writen(sockfd, sendbuf, strlen(sendbuf));
#if 0
sleep(3);
//測試對端關閉的情況下再次寫數據,造成客戶端產生SIGPIPE信號,該信號的默認動作是終止
writen(sockfd, "再次寫數據aaa....\n", 100);
#endif
//按照行讀數據
int ret = readline(sockfd, recvbuf, sizeof(recvbuf));
if (ret == -1)
ERR_EXIT("readline()");
else if (ret == 0)
{
printf("server close\n");
break;
}
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockfd);
}
void test()
{
int sockfd = 0;
const char *serverip = "192.168.66.128";
//若程序收到SIGPIPE,則忽略
signal(SIGPIPE, SIG_IGN);
//創建socket
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket()");
//定義socket結構體 man 7 ip
struct sockaddr_in srvsddr;
srvsddr.sin_family = AF_INET;
srvsddr.sin_port = htons(8001);//轉化爲網絡字節序
//第一種
#if 0
srvsddr.sin_addr.s_addr = inet_addr(serverip);
#endif
//第二種
#if 0
//srvsddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 就是0.0.0.0 不存在網絡字節序
//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //綁定本機的任意一個地址
#endif
//第三種
//建議使用這種
#if 1
int ret;
ret = inet_pton(AF_INET, serverip, &srvsddr.sin_addr);
if (ret == 0)
{
ERR_EXIT("inet_pton()");
}
#endif
//進程-》內核
if (connect(sockfd, (struct sockaddr*)&srvsddr, sizeof(srvsddr)) < 0)
ERR_EXIT("connect");
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
//內核-》進程
//獲取本地的地址 注意是已連接以後的套接字
if ((getsockname(sockfd, (struct sockaddr *)&localaddr, &addrlen)) < 0)
ERR_EXIT("getsockname()");
printf("本機的ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
echo_cli(sockfd);
return ;
}
int main()
{
test();
return 0;
}
10server_readline.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
/*
包尾加上\n編程實踐
*/
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
/*
使用說明:
//1一次全部讀走 //2次讀完數據 //出錯分析 //對方已關閉
思想:
tcpip是流協議,不能保證1次讀操作,能全部把報文讀走,所以要循環
讀指定長度的數據。
按照count大小讀數據,若讀取的長度ssize_t<count 說明讀到了一個結束符,
對方已關閉
函數功能:
從一個文件描述符中讀取count個字符到buf中
參數:
@buf:接受數據內存首地址
@count:接受數據長度
返回值:
@ssize_t:返回讀的長度 若ssize_t<count 讀失敗失敗
*/
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; //剩下需要讀取的數據個數
ssize_t nread; //成功讀取的字節數
char * bufp = (char*)buf;//將參數接過來
while (nleft > 0)
{
//如果errno被設置爲EINTR爲被信號中斷,如果是被信號中斷繼續,
//不是信號中斷則退出。
if ((nread = read(fd, bufp, nleft)) < 0)
{
//異常情況處理
if (errno == EINTR) //讀數據過程中被信號中斷了
continue; //再次啓動read
//nread = 0;//等價於continue
return -1;
}else if (nread == 0) //到達文件末尾EOF,數據讀完(讀文件、讀管道、socket末尾、對端關閉)
break;
bufp += nread; //將字符串指針向後移動已經成功讀取個數的大小。
nleft -=nread; //需要讀取的個數=需要讀取的個數-已經成功讀取的個數
}
return (count - nleft);//返回已經讀取的數據個數
}
/*
思想:tcpip是流協議,不能1次把指定長度數據,全部寫完
按照count大小寫數據
若讀取的長度ssize_t<count 說明讀到了一個結束符,對方已關閉。
函數功能:
向文件描述符中寫入count個字符
函數參數:
@buf:待寫數據首地址
@count:待寫長度
返回值:
@ssize_t:返回寫的長度 -1失敗
*/
ssize_t writen(int fd, 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 ((nwritten < 0) && (errno == EINTR)) //讀數據過程中被信號中斷了
continue; //再次啓動write
//nwritten = 0; //等價continue
else
return -1;
}
bufp += nwritten; //移動緩衝區指針
nleft -=nwritten; //記錄剩下未讀取的數據
}
return count;//返回已經讀取的數據個數
}
//從指定的socket中讀取指定大小的數據但不取出,封裝後不被信號中斷
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
//MSG_PEEK 讀取隊列中指定大小的數據,但不取出
int ret = recv(sockfd, buf, len, MSG_PEEK);
//如果被信號中斷,則繼續
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
/*
maxline 一行最大數
先提前peek一下緩衝區,如果有數據從緩衝區讀數據,
1、緩衝區數據中帶\n
2 、緩存區中不帶\n
讀取數據包直到\n
*/
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread; //成功預讀取的數據個數
char *bufp = buf; //讀取數據存放的數組,在外分配內存
int nleft = maxline;//封包最大值
while (1)
{
//看一看緩衝區有沒有數據,並不移除內核緩衝區數據
ret = recv_peek(sockfd, bufp, 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') //若緩衝區有\n
{
ret = readn(sockfd, bufp, i+1);//讀走數據
if (ret != i + 1)
exit(EXIT_FAILURE);
return ret;//有\n就返回,並返回讀走的數據
}
}
//如果讀到的數大於 一行最大數 異常處理
if (nread > nleft)
exit(EXIT_FAILURE);
nleft -= nread;//若緩衝區沒有\n, 把剩餘的數據讀走
ret = readn(sockfd, bufp, nread);
if (ret != nread)
exit(EXIT_FAILURE);
bufp += nread;//bufp指針後移後,再接着偷看緩衝區數據recv_peek,直到遇到\n
}
return -1;
}
void do_service(int conn)
{
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = readline(conn, recvbuf, sizeof(recvbuf));
if (ret == -1)
ERR_EXIT("readline()");
if (ret == 0)
{
printf("client close\n");
break;
}
//將數據打印輸出
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
if (recvbuf[0] == '2') //注意2 一共2處。。。。
{
//如果父進程沒有關閉,則客戶端應該關閉2次,才能往對端發送FIN分節
//close(conn); //11111111111
//close(conn); //11111111111
shutdown(conn, SHUT_WR);
}
}
}
//正確的使用方法
void handle_sigchld2(int signo)
{
int mypid;
while (( mypid = waitpid(-1, NULL, WNOHANG)) > 0)
{
printf("孩子退出,父進程要收屍:%d\n", mypid);
}
}
void test()
{
int sockfd = 0;
int conn = 0;
const char *serverip = "192.168.66.128";
//安裝信號處理函數 使用waitpid
#if 1
signal(SIGCHLD, handle_sigchld2);
#endif
//創建socket
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket()");
//定義socket結構體 man 7 ip
struct sockaddr_in srvsddr;
srvsddr.sin_family = AF_INET;
srvsddr.sin_port = htons(8001);//轉化爲網絡字節序
//第一種
#if 0
srvsddr.sin_addr.s_addr = inet_addr(serverip);
#endif
//第二種
#if 0
//srvsddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 就是0.0.0.0 不存在網絡字節序
//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //綁定本機的任意一個地址
#endif
//第三種
//建議使用這種
#if 1
int ret;
ret = inet_pton(AF_INET, serverip, &srvsddr.sin_addr);
if (ret == 0)
{
ERR_EXIT("inet_pton()");
}
#endif
//設置端口複用
//使用SO_REUSEADDR選項可以使得不必等待TIME_WAIT狀態消失就可以重啓服務器
int optval = 1;
if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
ERR_EXIT("setsockopt()");
if(bind(sockfd, (struct sockaddr *)&srvsddr,sizeof(srvsddr)) <0 )
ERR_EXIT("bind()");
if(listen(sockfd, SOMAXCONN) < 0)
ERR_EXIT("listen()");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);//值-結果參數
pid_t pid;
while (1)
{
if ((conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
ERR_EXIT("accept()");
printf("客戶端1 ip:%s port:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
#if 0
//方法2 只要能拿到連接socket就行
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
//注意是已連接以後的套接字
if (getpeername(conn, (struct sockaddr*)&clientaddr, &clientlen) < 0)
ERR_EXIT("getpeername");
printf("客戶端2 ip=%s port=%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
#endif
pid = fork();
if (pid == -1)
{
ERR_EXIT("fork()");
}
if (pid == 0)
{
//子進程不需要監聽socket
close(sockfd);
do_service(conn);
exit(EXIT_SUCCESS);
}else
{
//必須要關閉掉,因爲close()函數使用了引用計數技術
//close(conn);//父進程不需要連接socket 注意1.。。
}
}
return ;
}
int main()
{
test();
return 0;
}