說明
tcp 字節流 無邊界
udp 消息、數據報 有邊界
對等方,一次讀操作,不能保證完全把消息讀完。
對方接受數據包的個數是不確定的。
產生粘包問題的原因
1、SQ_SNDBUF 套接字本身有緩衝區 (發送緩衝區、接受緩衝區)
2、tcp傳送的端 mss大小限制
3、鏈路層也有MTU大小限制,如果數據包大於>MTU要在IP層進行分片,導致消息分割。
4、tcp的流量控制和擁塞控制,也可能導致粘包
5、tcp延遲發送機制 等等
結論:tcp/ip協議,在傳輸層沒有處理粘包問題。
粘包解決方案
本質上是要在應用層維護消息與消息的邊界
定長包
包尾加\r\n(ftp)
包頭加上包體長度
更復雜的應用層協議
例1:包頭加上包體長度編程實踐
包頭加上包體長度
發報文時,前四個字節長度(轉成網絡字節序)+包體
收報文時,先讀前四個字節,求出長度;根據長度讀數據。
7client_stick1.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>
/*
包頭+包體編程
*/
/*
*******readn、writen、readline屬於同一個系列,稱爲網絡編程三大函數*******
1.1 read與write原型
ssize_t read(int fd, void* buf, size_t count);
ssize_t 是有符號整數
返回值如下:
a)成功返回讀取的字節數,這裏可能等於 count 或者小於 count
(當 count > 文件 size 的時候,返回實際讀到的字節數);
b)剛開始讀就遇到EOF 則返回 0;
c)讀取失敗返回 -1, 並設置相應的 errno
ssize_t write(int fd, const void *buf, size_t count);
返回值如下:
a)成功返回寫入的字節數,這裏同上;
b)寫入失敗返回 -1,並設置相應的 errno;
c)當返回值爲0 時,表示什麼也沒有寫進去,這種情況在socket編程中出現可能是
因爲連接已關閉,在寫磁盤文件的時候一般不會出現。
1.2 爲什麼要封裝一個readn 函數和 writen 函數,現有的read 函數和 write 含有有什麼缺陷?
這個是因爲在調用read(或 write)函數的時候,讀(寫)一次的返回值可能不是我們想到讀
的字節數(即read函數中的 count 參數),這經常在讀取管道,或者網絡數去時出現。
1.3 readn 函數 和 writen 函數
1.3.1 readn保證在沒有遇到EOF的情況下,一定可以讀取n個字節。它的返回值有三種:
a) >0,表示成功讀取的字節數,如果小於n,說明中間遇到了EOF;
b)==0 表示一開始讀取就遇到EOF;
c) -1 表示錯誤(這裏的errno絕對不是EINTR)
1.3.2 writen函數保證一定寫滿n個字節,返回值:
a)n 表示寫入成功n個字節
b)-1 寫入失敗(這裏也沒有EINTR錯誤)
*/
/*
tcp粘包處理:包頭+包體長度
*/
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
struct packet
{
int len; //長度在前,首地址與結構體的首地址是一致的
char buf[1024]; //數據在後
} packet;
/*
使用說明:
//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;//返回已經讀取的數據個數
}
void test()
{
int sockfd = 0;
const char *serverip = "192.168.66.128";
//創建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);//轉化爲網絡字節序
srvsddr.sin_addr.s_addr = inet_addr(serverip);
//進程-》內核
if( connect(sockfd, (struct sockaddr *)&srvsddr,sizeof(srvsddr)) < 0)
ERR_EXIT("connect()");
size_t n;
struct packet sendbuf;
struct packet recvbuf;
memset(&sendbuf, 0, sizeof(sendbuf));
memset(&recvbuf, 0, sizeof(recvbuf));
while( fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL )
{
//獲取字符串的大小
n = strlen(sendbuf.buf);
//將n轉換爲網絡字節序
sendbuf.len = htonl(n);
//將獲取的包發送
writen(sockfd, &sendbuf, 4 + n);
//第一次 先讀取數據包的長度
ssize_t ret = readn(sockfd, &recvbuf.len, 4);
if (ret == -1)
{
ERR_EXIT("readn");
}else if (ret < 4)
{
printf("client close\n");
break;
}
//轉換爲本地字節存儲
n = ntohl(recvbuf.len);
//第二次 根據數據長度讀取所有數據
ret = readn(sockfd, recvbuf.buf, n);
if (ret == -1)
{
ERR_EXIT("readn");
}else if (ret < n)
{
//因爲告訴了數據的長度,所以ret等於n纔是正確的
printf("client close\n");//對於網絡來說,意味着對端關閉了
break;
}
fputs(recvbuf.buf, stdout);
memset(&sendbuf, 0, sizeof(sendbuf));
memset(&recvbuf, 0, sizeof(recvbuf));
}
//異常處理
close(sockfd);
return ;
}
int main()
{
test();
return 0;
}
8server_stick1.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>
/*
包頭+包體編程
*/
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
struct packet
{
int len; //長度在前,首地址與結構體的首地址是一致的
char buf[1024]; //數據在後
};
/*
使用說明:
//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;//返回已經讀取的數據個數
}
void do_service(int conn)
{
struct packet recvbuf;
int n;
while (1)
{
memset(&recvbuf, 0, sizeof(recvbuf));
//讀取包頭 4個字節
int ret = readn(conn, &recvbuf.len, 4);
if (ret == -1)
ERR_EXIT("read()");
//如果讀取的個數小於4,則客服端已經關閉
else if (ret < 4)
{
printf("client close\n");
break;
}
//將網絡數據轉換爲本地數據結構,比如網絡數據爲大端,而本地數據爲小端
n = ntohl(recvbuf.len);
//根據包頭裏包含的數據大小讀取數據
ret = readn(conn, recvbuf.buf, n);
if (ret < -1)
ERR_EXIT("read");
else if (ret < n)
{
printf("client close\n");
break;
}
//將數據打印輸出
fputs(recvbuf.buf, stdout);
//將接收到的數據再直接發出去
writen(conn, &recvbuf, 4 + n);//注意寫數據的時候,多加4個字節
}
}
void test()
{
int sockfd = 0;
int conn = 0;
const char *serverip = "192.168.66.128";
//創建socket
//創建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("客戶端的ip:%s port:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
pid = fork();
if (pid == -1)
{
ERR_EXIT("fork()");
}
if (pid == 0)
{
//子進程不需要監聽socket
close(sockfd);
do_service(conn);
exit(EXIT_SUCCESS);
}else
{
close(conn);//父進程不需要連接socket
}
}
return ;
}
int main()
{
test();
return 0;
}