Socket编程实践(7)   --TCP粘包解决方法2

包尾加\n编程实践

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

与read相比,只能用于套接字文件描述符,而且多了一个flags

Flags常用取值:

MSG_OOB(紧急指针,带外数据)

This flag requests receipt of out-of-band data that would not be received  in the normal data stream.  Some protocols place expedited data at the head of the normal data queue, and  thus  this flag cannot be used with such protocols.

MSG_PEEK(可以读数据,不从缓存区中读走,利用此特点可以方便的实现按行读取数据;一个一个字符的读,方法不好;多次调用系统调用read方法)

This  flag  causes the receive operation to return data from the beginning of the receive queue without removing that  data  from the queue.  Thus, a subsequent receive call will return the same data.

1.原客户端代码分析

  ....
    //从键盘输入数据:调用fgets函数,行尾的/n是默认自带的,请参看下图gdb调试的截图
    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) == -1)
        {
            err_exit("write socket error");
        }
.....

2.readline函数实现及解析

//只是查看一下网络中的数据,并不是将之真正取走:MSG_PEEK
ssize_t recv_peek(int fd, void *buf, size_t count)
{
    int nRead = 0;
    //如果读取网络数据出错,则继续读取
    while ((nRead = recv(fd,buf,count,MSG_PEEK)) == -1);
    return nRead;
}

ssize_t readline(int fd, void *buf, size_t maxline)
{
    char *pBuf = (char *)buf;
    int nLeft = maxline;

    while (true)
    {
        //查看缓冲区中的数据,并不真正取走
        int nTestRead = recv_peek(fd,pBuf,nLeft);

        //检测这次读来的数据中是否包含‘\n‘;
        //如果有,则将之全部读取出来
        for (int i = 0; i < nTestRead; ++i)
        {
            if (pBuf[i] == ‘\n‘)
            {
                //真正的从缓冲区中将数据取走
                if (readn(fd,pBuf,i+1) != i+1)
                {
                    err_exit("readn error");
                }
                else
                {
                    return i + 1;
                }
            }
        }

        //如果这次读的缓冲区中没有‘\n‘

        //如果读超了:读道德数目大于一行最大数,则做异常处理
        if (nTestRead > nLeft)
        {
            exit(EXIT_FAILURE);
        }

        nLeft -= nTestRead; //若缓冲区没有‘\n‘,则将剩余的数据读走
        if (readn(fd,pBuf,nTestRead) != nTestRead)
        {
            exit(EXIT_FAILURE);
        }

        pBuf += nTestRead;
    }

    return -1;
}

3.server端完整代码及解析

#include "commen.h"

//echo 服务器readline版
int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if (sockfd == -1)
    {
        err_exit("socket error");
    }

    //添加地址复用
    int optval = 1;
    if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1)
    {
        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;    //绑定本机的任意一个IP地址
    if (bind(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == -1)
    {
        err_exit("bind error");
    }

    //启动监听套接字
    if (listen(sockfd,SOMAXCONN) == -1)
    {
        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 == -1)
        {
            err_exit("accept error");
        }

        //打印客户信息
        cout << "Client:" << endl;
        cout << "\tsin_port: " << ntohs(peerAddr.sin_port) << endl;
        cout << "\tsin_addr: " << inet_ntoa(peerAddr.sin_addr) << endl;
        cout << "\tsocket: " << peerSockfd << endl;

        //每有一个客户端连接进来,就fork一个子进程,
        //相应的业务处理由子进程完成,父进程继续监听
        pid_t pid = fork();
        if (pid == -1)
        {
            close(sockfd);
            close(peerSockfd);
            err_exit("fork error");
        }
        else if (pid == 0)  //子进程,处理业务
        {
            close(sockfd);  //子进程关闭监听套接字,因为子进程不负责监听任务

            char recvBuf[BUFSIZ];
            ssize_t readCount = 0;
            while (true)
            {
                memset(recvBuf,0,sizeof(recvBuf));

                //读取一行数据(会根据数据流中的\n而终止读取)
                if ((readCount = readline(peerSockfd,recvBuf,sizeof(recvBuf))) == -1)
                {
                    err_exit("readn error");
                }
                else if (readCount == 0)
                {
                    peerClosePrint("client connect closed");
                }

                //将整体报文回写回客户端
                if (writen(peerSockfd,recvBuf,strlen(recvBuf)) == -1)
                {
                    err_exit("writen error");
                }

                recvBuf[readCount] = 0;
                //写至终端
                fputs(recvBuf,stdout);
            }
        }
        else if (pid > 0)   //父进程
        {
            close(peerSockfd);
        }
    }

    close(sockfd);
    return 0;
}

4.新版client端完整代码实现及解析

#include "commen.h"

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if (sockfd == -1)
    {
        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)) == -1)
    {
        err_exit("connect error");
    }

    int readCount = 0;
    char sendBuf[BUFSIZ];
    char recvBuf[BUFSIZ];

    //从键盘输入数据:调用fgets函数,行尾的/n是默认自带的,请参看下图gdb调试的截图
    while (fgets(sendBuf,sizeof(sendBuf),stdin) != NULL)
    {
        //向server发送数据(会自动附带\n)
        if (writen(sockfd,sendBuf,strlen(sendBuf)) == -1)
        {
            err_exit("write socket error");
        }

        //从server端接收一行数据
        if ((readCount = readline(sockfd,recvBuf,sizeof(recvBuf))) == -1)
        {
            err_exit("read socket error");
        }
        else if (readCount == 0)
        {
            peerClosePrint("client connect closed");
        }

        recvBuf[readCount] = 0;
        //将其回写到终端
        fputs(recvBuf,stdout);

        memset(sendBuf,0,sizeof(sendBuf));
        memset(recvBuf,0,sizeof(recvBuf));
    }

    close(sockfd);
    return 0;
}

附1-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;

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);
}

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)
        {
            //一点东西都没读
            if (nLeft == count)
            {
                return -1;  //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)
        {
            //一点东西都没写
            if (nLeft == count)
            {
                return -1;  //error
            }
            else
            {
                break;  //error, return amount write so far
            }
        }
        else if (nWritten == 0)
        {
            break;  //EOF
        }

        nLeft -= nWritten;
        ptr += nWritten;
    }

    return count - nWritten;
}

//只是查看一下网络中的数据,并不是将之真正取走:MSG_PEEK
ssize_t recv_peek(int fd, void *buf, size_t count)
{
    int nRead = 0;
    //如果读取网络数据出错,则继续读取
    while ((nRead = recv(fd,buf,count,MSG_PEEK)) == -1);
    return nRead;
}

ssize_t readline(int fd, void *buf, size_t maxline)
{
    char *pBuf = (char *)buf;
    int nLeft = maxline;

    while (true)
    {
        //查看缓冲区中的数据,并不真正取走
        int nTestRead = recv_peek(fd,pBuf,nLeft);

        //检测这次读来的数据中是否包含‘\n‘;
        //如果有,则将之全部读取出来
        for (int i = 0; i < nTestRead; ++i)
        {
            if (pBuf[i] == ‘\n‘)
            {
                //真正的从缓冲区中将数据取走
                if (readn(fd,pBuf,i+1) != i+1)
                {
                    err_exit("readn error");
                }
                else
                {
                    return i + 1;
                }
            }
        }

        //如果这次读的缓冲区中没有‘\n‘

        //如果读超了:读道德数目大于一行最大数,则做异常处理
        if (nTestRead > nLeft)
        {
            exit(EXIT_FAILURE);
        }

        nLeft -= nTestRead; //若缓冲区没有‘\n‘,则将剩余的数据读走
        if (readn(fd,pBuf,nTestRead) != nTestRead)
        {
            exit(EXIT_FAILURE);
        }

        pBuf += nTestRead;
    }

    return -1;
}

#endif // COMMEN_H_INCLUDED

附2-Mafile文件

CC = g++
CPPFLAGS = -Wall -g -pthread

BIN = server client
SOURCES = $(BIN.=.cpp)

.PHONY: clean all 

all: $(BIN)

$(BIN): $(SOURCES)

clean:
    -rm -rf $(BIN) bin/ obj/ core
时间: 2024-10-13 20:45:35

Socket编程实践(7)   --TCP粘包解决方法2的相关文章

Socket编程实践(5) --TCP粘包问题与解决

TCP粘包问题 因为TCP协议是基于字节流且无边界的传输协议, 因此非常有可能产生粘包问题, 问题描写叙述例如以下 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvempmMjgwNDQxNTg5/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" /> 对于Host A 发送的M1与M2两个各10K的数据块, Host B 接收数据的方式不确定, 有以下

Socket编程实践(6) --TCP粘包原因与解决

流协议与粘包 粘包的表现 Host A 发送数据给 Host B; 而Host B 接收数据的方式不确定 粘包产生的原因 说明 TCP 字节流,无边界 对等方,一次读操作,不能保证完全把消息读完 UDP 数据报,有边界 对方接受数据包的个数是不确定的 产生粘包问题的原因分析 1.SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区.接受缓冲区) 2.tcp传送的端 mss大小限制 3.链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割. 4.tcp的流量控制和拥塞控

Socket编程实践(6) --TCP服务端注意事项

僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 如果多个客户端同时关闭, 问题描述如下面两幅图所示: /** client端实现的测试代码**/ int main() { int s

socket tcp 粘包解决

何为粘包: 先看代码 session=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 在定义socket对象的时候 有两个参数 一个是   socket地址家族,另一个是处理类型socket.SOCK_STREAM,注意是  'stream':流 那既然是流处理类型,理解上就是 水流式  处理数据. 这个时候数据是没有边界(也就是没有从头开始,到哪里)的概念就像下图 现在执行命令很正常: 执行了一个cat /etc/passwd   , 也能显示

Socket编程实践(1) --TCP/IP简述

ISO的OSI OSI(open system interconnection)开放系统互联模型是由ISO国际标准化组织定义的网络分层模型,共七层, 从下往上为: OSI七层参考模型 物理层(Physical Layer) 物理层定义了所有电子及物理设备的规范,为上层的传输提供了一个物理介质,本层中数据传输的单位为比特(bit/二进制位).属于本层定义的规范有EIA/TIA RS-232.RJ-45等,实际使用中的设备如网卡属于本层. 数据链路层(Data Link Layer) 对物理层收到的

Socket编程实践(17) --TCP/IP各层报文(2)

UDP数据报 UDP首部代码: struct udp_hdr { unsigned short src_port; unsigned short dest_port; unsigned short len; unsigned short chksum; }; TCP报文段 协议描述 源端口号和目的端口号:源和目的主机的IP地址加上端口号构成一个TCP连接 序号和确认号:序号为该TCP数据包的第一个数据字在所发送的数据流中的偏移量:确认号为希望接收的下一个数据字的序号: 首部长度,以4个字节为单位

Socket编程实践(9) --TCP服务器常见问题(4)

TCP/IP协议的11种状态 说明: 1.如下图(客户端与服务器都在本机:双方(server的子进程,与client)链接已经建立(ESTABLISHED),等待通信) 2.最先调用close的一端,后面会进入TIME_WAIT的状态(下图,server端首先关闭) 3.TIME_WAIT 时间是2MSL(报文的最长存活周期的2倍)     原因:(ACK y+1)如果发送失败可以重发. 服务器端处于closed状态,不等于客户端也处于closed状态.. 4.TCP/IP协议的第1种状态:图上

Socket编程实践(16) --TCP/IP各层报文(1)

以太网帧格式 说明1:链路层的数据包,称为以太网帧. 说明2:链路层不识别IP地址[因为IP地址是逻辑地址],链路层识别物理网卡MAC地址[硬件地址]. 说明3:需要根据IP地址找到对方的MAC地址(ARP地址解析协议)[MAC -> IP地址方向地址解析:RARP反向地址解析协议]. 说明4:应用层根据对等方的IP地址进行通讯,在数据封装过程中,链路层需要目的地址的MAC地址从何而来?需要将IP地址转换成MAC地址,也就是地址解析. 以太网首部代码: struct ethernet_hdr {

Linux下的socket编程实践(四)TCP的粘包问题和常用解决方案

TCP粘包问题的产生 由于TCP协议是基于字节流并且无边界的传输协议, 因此很有可能产生粘包问题.此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段.若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,但是接收方并不知道要一次接收多少字节的数据,这样接收方就收到了粘包数据.具体可以见下图: 假设主机A send了两条消息M1和M2 各10k 给主机B,由于主机B一次提取的字节数