Socket編程實踐(6) --TCP服務器常見問題(1)
來源:程序員人生 發布時間:2014-12-17 08:21:14 閱讀次數:2801次
流協議與粘包
粘包的表現
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
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈