1.流協議與粘(nian)包
-
tcp是基於字節流的傳輸服務(字節流是無邊界的),像流水一樣,無法區分邊界,他不能保證對等方一次讀操作能夠返回多少字節。
eg:hostA發送兩個數據包給hostB,對於hostB來講,他可能有以下四種情況:例如第(2),一次性讀取M1和M2的所有消息,這裏M1和M2就粘在一起了。
第(3)第一次讀操作返回M1消息的全部和M2條消息的一部分(M2_1),第二次讀操作返回M2條消息的一部分(M2_2) -
udp是基於消息的報文,是有邊界的
2.粘包產生的原因
- tcp會有粘包問題
(1)write將應用進程緩衝區的數據拷貝到套接口發送緩衝區中,當應用進程緩衝區的大小超過了套接口發送緩衝區SO_SNDBUF的大小,就有可能產生粘包問題,可能一部分已經發送出去了,對方已經接收,另外一部分才發送套接口發送緩衝區進行發送,對方延遲接收後一部分數據,導致粘包問題,這裏的原因是:數據包的分割
(2)TCP最大段MSS的限制,可能會對發送的消息進行分割,可能會產生粘包問題
(3)應用層的最大傳輸單元MTU的限制,若發送的數據包超過MTU,會在IP層進行分組或分片,可能會對發送的消息進行分割,可能會產生粘包問題
(4)tcp的流控,擁塞控制,延遲發送機制都有可能產生粘包問題
4.粘包處理方案
- 本質上是要在應用層維護消息與消息的邊界
(1)定長包
(2)包尾加\r\n(ftp就是這麼用的)
消息若本來就有\r\n,\r\n本來就是消息的一部分的話,則需要轉義的方式
(3)包頭加上包體長度
可以先接收包頭,然後通過包頭計算出包體的長度,然後才接收包體所對應的數據包
(4)更復雜的應用層協議
5.readn,writen
- 具體實現已經在6中回射服務器中了
6.回射客戶/服務器
- (1)以定長的方式收發數據
NetworkProgramming-master (1)\LinuxNetworkProgramming\P9echosrv.c
NetworkProgramming-master (1)\LinuxNetworkProgramming\P9echocli.c
==================NetworkProgramming-master (1)\LinuxNetworkProgramming\P9echosrv.c=============================
//
// Created by wangji on 19-7-21.
//
#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);
//參考man 2 read聲明寫出來的
//ssize_t是無符號整數
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;//已接收字節數
char *bufp = (char*) buf;
while (nleft > 0)
{
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
}
else if (nread == 0)//表示讀取到了EOF,表示對方關閉
return count - nleft;//表示剩餘的字節數
bufp += nread;//讀到的nread,要將bufp指針偏移
nleft -= nread;
}
return count;
}
//參考man 2 write聲明寫出來的
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)
{
//write一般不會阻塞,緩衝區數據大於發送的數據,就能夠成功將數據拷貝到緩衝區中
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;//已發送字節數
nleft -= nwritten;//剩餘字節數
}
return count;
}
void do_service(int connfd)
{
// char recvbuf[1024];
struct packet recvbuf;
int n;
while (1)
{
memset(&recvbuf, 0, sizeof recvbuf);
int ret = readn(connfd, &recvbuf.len, 4);
if (ret == -1)
{
ERR_EXIT("read");
}
else if (ret < 4)
{
printf("client close\n");
break;
}
n = ntohl(recvbuf.len);
ret = readn(connfd, recvbuf.buf, n);
if (ret == -1)
{
ERR_EXIT("read");
}
else if (ret < n)
{
printf("client close\n");
break;
}
fputs(recvbuf.buf, stdout);
writen(connfd, &recvbuf, 4+n);
}
}
int main(int argc, char** argv) {
// 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);
do_service(connfd);
//printf("child exit\n");
exit(EXIT_SUCCESS);
}
else
{
//printf("parent exit\n");
close(connfd);
}
}
// 7. 斷開連接
close(listenfd);
return 0;
}
==================NetworkProgramming-master (1)\LinuxNetworkProgramming\P9echocli.c=============================
//
// Created by wangji on 19-7-21.
//
#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;
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
//參考man 2 read聲明寫出來的
//ssize_t是無符號整數
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;//已接收字節數
char *bufp = (char*) buf;
while (nleft > 0)
{
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
}
else if (nread == 0)//表示讀取到了EOF,表示對方關閉
return count - nleft;//表示剩餘的字節數
bufp += nread;//讀到的nread,要將bufp指針偏移
nleft -= nread;
}
return count;
}
//參考man 2 write聲明寫出來的
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)
{
//write一般不會阻塞,緩衝區數據大於發送的數據,就能夠成功將數據拷貝到緩衝區中
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;//已發送字節數
nleft -= nwritten;//剩餘字節數
}
return count;
}
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");
}
// 4. 數據交換
char recvbuf[1024]={0};
char sendbuf[1024]={0};
int n = 0;
while (fgets((sendbuf), sizeof(sendbuf), stdin) != NULL) // 鍵盤輸入獲取
{
//發送定長包,缺點:若發送數據小,但是發的是定長包,會對網絡造成負擔
writen(sockfd, sendbuf,sizeof(sendbuf));//發送和接收都是1024個字節
readn(sockfd, recvbuf,sizeof(recvbuf));
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
// 5. 斷開連接
close(sockfd);
return 0;
}
- (2)發送的數據包是頭部+包體,接收的時候:先接收包頭,接收完畢後,將數據包的長度計算出來,再接收對應的長度,對消息與消息之間進行了區分
NetworkProgramming-master (1)\LinuxNetworkProgramming\P9echosrv3.c
NetworkProgramming-master (1)\LinuxNetworkProgramming\P9echocli3.c
=====================================NetworkProgramming-master (1)\LinuxNetworkProgramming\P9echosrv3.c=============
//
// Created by wangji on 19-7-21.
//
#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);
//參考man 2 read聲明寫出來的
//ssize_t是無符號整數
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;//已接收字節數
char *bufp = (char*) buf;
while (nleft > 0)
{
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
}
else if (nread == 0)//表示讀取到了EOF,表示對方關閉
return count - nleft;//表示剩餘的字節數
bufp += nread;//讀到的nread,要將bufp指針偏移
nleft -= nread;
}
return count;
}
//參考man 2 write聲明寫出來的
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)
{
//write一般不會阻塞,緩衝區數據大於發送的數據,就能夠成功將數據拷貝到緩衝區中
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;//已發送字節數
nleft -= nwritten;//剩餘字節數
}
return count;
}
void do_service(int connfd)
{
// char recvbuf[1024];
struct packet recvbuf;
int n;
while (1)
{
memset(&recvbuf, 0, sizeof recvbuf);
int ret = readn(connfd, &recvbuf.len, 4);//先接收頭部
if (ret == -1)
{
ERR_EXIT("read");
}
else if (ret < 4)
{
printf("client close\n");
break;
}
n = ntohl(recvbuf.len);//轉換程主機字節序,包體實際長度n
ret = readn(connfd, recvbuf.buf, n);//接收包體
if (ret == -1)
{
ERR_EXIT("read");
}
else if (ret < n)
{
printf("client close\n");
break;
}
fputs(recvbuf.buf, stdout);
writen(connfd, &recvbuf, 4+n);
}
}
int main(int argc, char** argv) {
// 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);
do_service(connfd);
//printf("child exit\n");
exit(EXIT_SUCCESS);
}
else
{
//printf("parent exit\n");
close(connfd);
}
}
// 7. 斷開連接
close(listenfd);
return 0;
}
=================================NetworkProgramming-master (1)\LinuxNetworkProgramming\P9echocli3.c=============
//
// Created by wangji on 19-7-21.
//
#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);
/參考man 2 read聲明寫出來的
//ssize_t是無符號整數
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;//已接收字節數
char *bufp = (char*) buf;
while (nleft > 0)
{
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
}
else if (nread == 0)//表示讀取到了EOF,表示對方關閉
return count - nleft;//表示剩餘的字節數
bufp += nread;//讀到的nread,要將bufp指針偏移
nleft -= nread;
}
return count;
}
//參考man 2 write聲明寫出來的
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)
{
//write一般不會阻塞,緩衝區數據大於發送的數據,就能夠成功將數據拷貝到緩衝區中
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;//已發送字節數
nleft -= nwritten;//剩餘字節數
}
return count;
}
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");
}
// 4. 數據交換
// 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.buf, sizeof(sendbuf.buf), stdin) != NULL) // 鍵盤輸入獲取
{
n = strlen(sendbuf.buf);//n是包體的長度
sendbuf.len = htonl(n); // 主機字節序轉換爲網絡字節序
writen(sockfd, &sendbuf, 4+n); // 頭部4字節+包體
int ret = readn(sockfd, &recvbuf.len, 4); //先接收頭部
if (ret == -1)
{
ERR_EXIT("read");
}
else if (ret < 4)
{
printf("server close\n");
break;
}
n = ntohl(recvbuf.len);//轉換程主機字節序
ret = readn(sockfd, &recvbuf.buf, n);//接收包體
if (ret == -1)
{
ERR_EXIT("read");
}
else if (ret < n)
{
printf("server close\n");
break;
}
fputs(recvbuf.buf, stdout); //將接收到的數據輸出
// 清空
memset(&recvbuf, 0, sizeof recvbuf);
memset(&sendbuf, 0, sizeof sendbuf);
}
// 5. 斷開連接
close(sockfd);
return 0;
}
- 測試:
- Makefile
.PHONY:clean all
CC=gcc
CFLAGS=-Wall -g
BIN=echosrv echocli
all:$(BIN)
%.o:%.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f *.o $(BIN)
tcp是基於字節流的傳輸服務(字節流是無邊界的),像流水一樣,無法區分邊界,他不能保證對等方一次讀操作能夠返回多少字節。
eg:hostA發送兩個數據包給hostB,對於hostB來講,他可能有以下四種情況:例如第(2),一次性讀取M1和M2的所有消息,這裏M1和M2就粘在一起了。
(3)第一次讀操作返回M1消息的全部和M2條消息的一部分(M2_1),第二次讀操作返回M2條消息的一部分(M2_2)
udp是基於消息的報文,是有邊界的
tcp會有粘包問題
(1)write將應用進程緩衝區的數據拷貝到套接口發送緩衝區中,當應用進程緩衝區的大小超過了套接口發送緩衝區SO_SNDBUF的大小,就有可能產生粘包問題,可能一部分已經發送出去了,對方已經接收,另外一部分才發送套接口發送緩衝區進行發送,對方延遲接收後一部分數據,導致粘包問題,這裏的原因是:數據包的分割
(2)TCP最大段MSS的限制,可能會對發送的消息進行分割,可能會產生粘包問題
(3)應用層的最大傳輸單元MTU的限制,若發送的數據包超過MTU,會在IP層進行分組或分片,可能會對發送的消息進行分割,可能會產生粘包問題
(4)tcp的流控,擁塞控制,延遲發送機制都有可能產生粘包問題
消息若本來就有\r\n,\r\n本來就是消息的一部分的話,則需要轉義的方式
可以先接收包頭,然後通過包頭計算出包體的長度,然後才接收包體所對應的數據包
以定長的方式收發數據
發送的數據包是頭部+包體,接收的時候:先接收包頭,接收完畢後,將數據包的長度計算出來,再接收對應的長度,對消息與消息之間進行了區分