Socket编程实践(6) --TCP服务器常见问题(1)
来源:程序员人生 发布时间:2014-12-17 08:21:14 阅读次数:2649次
流协议与粘包
粘包的表现
Host A 发送数据给 Host B; 而Host B 接收数据的方式不肯定
粘包产生的缘由
说明
TCP | 字节流,无边界 | 对等方,1次读操作,不能保证完全把消息读完 |
UDP | 数据报,有边界 | 对方接受数据包的个数是不肯定的 |
产生粘包问题的缘由分析
1、SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区、接受缓冲区)
2、tcp传送的端 mss大小限制
3、链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,致使消息分割。
4、tcp的流量控制和堵塞控制,也可能致使粘包
5、tcp延迟发送机制等
结论:tcp/ip协议,在传输层没有处理粘包问题。
粘包解决方案(本质上是要在利用层保护消息与消息的边界)
定长包
包尾加
(ftp)
包头加上包体长度(以下)
更复杂的利用层协议
编程实践-readn && writen
管道,FIFO和某些装备(特别是终端和网络)有以下两种性质:
1)1次read操作所返回的数据可能少于所要求的数据,即便还没到达文件尾端也可能这样,但这不是1个毛病,应当继续读该装备;
2)1次write操作的返回值也可能少于指定输入的字节数.这多是由于某个因素酿成的,如:内核缓冲区满...但这也不是1个毛病,应当继续写余下的数据(通常,只有非阻塞描写符,或捕捉到1个信号时,才产生这类write的中途返回)
在读写磁盘文件时从未见到过这类情况,除非是文件系统用完了空间,或接近了配额限制,不能将所要求写的数据全部写出!
通常,在读,写1个网络装备,管道或终端时,需要斟酌这些特性.因而,我们就有了下面的这两个函数:readn和writen,功能分别是读写指定的N字节数据,并处理返回值可能小于要求值的情况:
ssize_t readnint fd, void *buf, size_t count);
ssize_t writen(int fd, const void *buf, size_t count);
返回值:
读写的字节数;若出错,返回⑴
实现:
这两个函数只是按需屡次调用read和write系统调用直至读写了N个数据
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nLeft = count;
ssize_t nRead = 0;
char *ptr = static_cast<char *>(buf);
while (nLeft > 0)
{
if ((nRead = read(fd,ptr,nLeft)) < 0)
{
//1点东西都没读
if (nLeft == count)
{
return ⑴; //error
}
else
{
break; //error, return amount read so far
}
}
else if (nRead == 0)
{
break; //EOF
}
nLeft -= nRead;
ptr += nRead;
}
return count - nLeft;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nLeft = count;
ssize_t nWritten;
const char *ptr = static_cast<const char *>(buf);
while (nLeft > 0)
{
if ((nWritten = write(fd,ptr,nLeft)) < 0)
{
//1点东西都没写
if (nLeft == count)
{
return ⑴; //error
}
else
{
break; //error, return amount write so far
}
}
else if (nWritten == 0)
{
break; //EOF
}
nLeft -= nWritten;
ptr += nWritten;
}
return count - nWritten;
}
报头加上报文长度编程实践
报文结构:
struct TransStruct
{
int m_length; //报头:保存数据m_text的真实数据长度
char m_text[BUFSIZ]; //报文:保存真正要发送的数据
};
发报文时:前4个字节长度+报文
收报文时:先读前4个字节,求出长度;根据长度读数据。
//server端完全代码及解析
#include "commen.h"
//echo
服务器writen,readn 版
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd == ⑴)
{
err_exit("socket error");
}
//添加地址复用
int optval = 1;
if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == ⑴)
{
err_exit("setsockopt SO_REUSEADDR error");
}
//绑定
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8002);
serverAddr.sin_addr.s_addr = INADDR_ANY; //绑定本机的任意1个IP地址
if (bind(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == ⑴)
{
err_exit("bind error");
}
//启动监听套接字
if (listen(sockfd,SOMAXCONN) == ⑴)
{
err_exit("listen error");
}
struct sockaddr_in peerAddr;
socklen_t peerLen = sizeof(peerAddr);
while (true)
{
//接受链接
int peerSockfd = accept(sockfd, (struct sockaddr *)&peerAddr,&peerLen);
if (peerSockfd == ⑴)
{
err_exit("accept error");
}
//打印客户信息
cout << "Client:" << endl;
cout << " sin_port: " << ntohs(peerAddr.sin_port) << endl;
cout << " sin_addr: " << inet_ntoa(peerAddr.sin_addr) << endl;
cout << " socket: " << peerSockfd << endl;
//每有1个客户端连接进来,就fork1个子进程,
//相应的业务处理由子进程完成,父进程继续监听
pid_t pid = fork();
if (pid == ⑴)
{
close(sockfd);
close(peerSockfd);
err_exit("fork error");
}
else if (pid == 0) //子进程,处理业务
{
close(sockfd); //子进程关闭监听套接字,由于子进程不负责监听凭务
struct TransStruct recvBuf;
ssize_t readCount = 0;
while (true)
{
memset(&recvBuf,0,sizeof(recvBuf));
//首先,从客户端读取报头长度
if ((readCount = readn(peerSockfd,&(recvBuf.m_length),4)) == ⑴)
{
err_exit("readn error");
}
else if (readCount == 0) //如果链接关闭
{
peerClosePrint("client connect closed");
}
//根据报文实际长度,读取数据
if ((readCount = readn(peerSockfd,&(recvBuf.m_text),recvBuf.m_length)) == ⑴)
{
err_exit("readn error");
}
else if (readCount == 0)
{
peerClosePrint("client connect closed");
}
//将整体报文回写回客户端
if (writen(peerSockfd,&recvBuf,recvBuf.m_length+4) == ⑴)
{
err_exit("writen error");
}
recvBuf.m_text[recvBuf.m_length] = 0;
//写至终端
fputs(recvBuf.m_text,stdout);
}
}
else if (pid > 0) //父进程
{
close(peerSockfd);
}
}
close(sockfd);
return 0;
}
//client端完全代码实现及解析
#include "commen.h"
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd == ⑴)
{
err_exit("socket error");
}
//填写好
服务器地址及其端口号
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8002);
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == ⑴)
{
err_exit("connect error");
}
int readCount = 0;
struct TransStruct sendBuf;
struct TransStruct recvBuf;
//从键盘输入数据
while (fgets(sendBuf.m_text,sizeof(sendBuf.m_text),stdin) != NULL)
{
//保存的是真实报文的长度
sendBuf.m_length = strlen(sendBuf.m_text);
//向server发送数据....+4的缘由:需要添加报首的4个字节报头的长度
if (writen(sockfd,&sendBuf,sendBuf.m_length+4) == ⑴)
{
err_exit("write socket error");
}
//首先,从server端接收将要发送的数据报的长度
if ((readCount = readn(sockfd,&(recvBuf.m_length),4)) == ⑴)
{
err_exit("read socket error");
}
else if (readCount == 0)
{
peerClosePrint("client connect closed");
}
//然后,根据从server端读来的报文长度,读取报文
if ((readCount = readn(sockfd,&(recvBuf.m_text),recvBuf.m_length)) == ⑴)
{
err_exit("read socket error");
}
else if (readCount == 0)
{
peerClosePrint("client connect closed");
}
recvBuf.m_text[recvBuf.m_length] = 0;
//将其回写到终端
fputs(recvBuf.m_text,stdout);
memset(&sendBuf,0,sizeof(sendBuf));
memset(&recvBuf,0,sizeof(recvBuf));
}
close(sockfd);
return 0;
}
附-commen.h完全代码及解析
#ifndef COMMEN_H_INCLUDED
#define COMMEN_H_INCLUDED
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
//报文结构
struct TransStruct
{
int m_length; //报头:保存数据m_text的真实数据长度
char m_text[BUFSIZ]; //报文:保存真正要发送的数据
};
//出错退出
void err_exit(std::string str)
{
perror(str.c_str());
exit(EXIT_FAILURE);
}
//对端关闭链接退出
void peerClosePrint(std::string str = "peer connect closed")
{
cout << str << endl;
_exit(0);
}
//信号捕获函数:上1篇博客中的代码需要使用的
void onSignal(int signalNumber)
{
switch (signalNumber)
{
case SIGUSR1:
cout << "child receive SIGUSR1" << signalNumber << endl;
_exit(0);
case SIGUSR2:
cout << "parent receive SIGUSR2: " << signalNumber << endl;
_exit(0);
default:
cout << "RECV OTHRER SIGNAL" << endl;
}
}
//经典的readn函数(来源:APUE)
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nLeft = count;
ssize_t nRead = 0;
char *ptr = static_cast<char *>(buf);
while (nLeft > 0)
{
if ((nRead = read(fd,ptr,nLeft)) < 0)
{
//1点东西都没读
if (nLeft == count)
{
return ⑴; //error
}
else
{
break; //error, return amount read so far
}
}
else if (nRead == 0)
{
break; //EOF
}
nLeft -= nRead;
ptr += nRead;
}
return count - nLeft;
}
//经典的writen函数(来源:APUE)
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nLeft = count;
ssize_t nWritten;
const char *ptr = static_cast<const char *>(buf);
while (nLeft > 0)
{
if ((nWritten = write(fd,ptr,nLeft)) < 0)
{
//1点东西都没写
if (nLeft == count)
{
return ⑴; //error
}
else
{
break; //error, return amount write so far
}
}
else if (nWritten == 0)
{
break; //EOF
}
nLeft -= nWritten;
ptr += nWritten;
}
return count - nWritten;
}
#endif // COMMEN_H_INCLUDED
生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠