说明
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;
}