项目框架(2):Socket通讯的服务端类

目录

一、创建socket

 二、TCP和UDP

1、TCP和UDP的区别

TCP

2、TCP保证自身可靠的方式

3、UDP不可靠的原因

4、TCP和UDP的使用场景

5、UDP能实现可靠传输吗?

 三、主机字节序与网络字节序

 四、网络通讯中的各种结构体

 1、sockaddr结构体

 2、sockaddr_in结构体

 3、gethostbyname函数(域名/主机名/字符串IP转大端序)

四、字符串IP与大端序IP的转换(只能字符串IP转大端序)

五、socket通信的服务端类

头文件.h

服务端初始化函数,连接客户端函数,获得客户端ip函数

收发数据

发送数据

接收数据


一、创建socket

int socket (int domain, int type, int protocol);

成功返回一个有效的socket,失败返回-1,errno被设置

单个进程创建的socket数量受系统参数open files的限制。(ulimit -a查看)

 参数:

  • domain(通讯协议族):PF_INET 为IPv4互联网协议族(常用)
  • type(数据传输类型):
    • SOCK_STREAM面向连接的socket:1)数据不会丢失;2)数据的顺序不会错乱;3)双向通道。

    • SOCK_DGRAM 为无连接的socket:1)数据可能会丢失;2)数据的顺序可能会错乱;3)传输的效率更高。

  • protocol(最终使用的协议):

    • 在IPv4网络协议家族中,数据传输方式为SOCK_STREAM的协议只有IPPROTO_TCP,数据传输方式为SOCK_DGRAM的协议只有IPPROTO_UDP

 例子:

socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

 二、TCP和UDP

1、TCP和UDP的区别

TCP

a)TCP面向连接,通过三次握手建立连接,四次挥手断开连接;  面试的重点

b)TCP是可靠的通信方式,通过超时重传、数据校验等方式来确保数据无差错,不丢失,不重复,并且按序到达;

c)TCP把数据当成字节流,当网络出现波动时,连接可能出现响应延迟的问题;

d)TCP只支持点对点通信;

e)TCP报文的首部较大,为20字节;

f)TCP是全双工的可靠信道。

UDP

a)UDP是无连接的,即发送数据之前不需要建立连接,这种方式为UDP带来了高效的传输效率,但也导致无法确保数据的发送成功;

b)UDP以最大的速率进行传输,但不保证可靠交付,会出现丢失、重复等等问题;

c)UDP没有拥塞控制,当网络出现拥塞时,发送方不会降低发送速率;

d)UDP支持一对一,一对多,多对一和多对多的通信;

e)UDP报文的首部比较小,只有8字节;

f)UDP是不可靠信道。

2、TCP保证自身可靠的方式

a)数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;

b)到达确认:接收端接收到分片数据时,根据分片的序号向对端回复一个确认包;

c)超时重发:发送方在发送分片后开始计时,若超时却没有收到对端的确认包,将会重发分片;

d)滑动窗口:TCP 中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方不会再发送数据;

e)失序处理:TCP的接收端会把接收到的数据重新排序;

f)重复处理:如果传输的分片出现重复,TCP的接收端会丢弃重复的数据;

g)数据校验:TCP通过数据的检验和来判断数据在传输过程中是否正确。

3、UDP不可靠的原因

 没有上述TCP的机制,如果校验和出错,UDP会将该报文丢弃。

4、TCP和UDP的使用场景

TCP 使用场景

TCP实现了数据传输过程中的各种控制,适合对可靠性有要求的场景。

UDP 使用场景

可以容忍数据丢失的场景:

  1. 视频、音频等多媒体通信(即时通信);
  2. 广播信息。

5、UDP能实现可靠传输吗?

 如果用UDP实现可靠传输,那么,应用程序必须实现重传和排序等功能,非常麻烦。

 三、主机字节序与网络字节序

  1. 大端序(Big Endian):低位字节存放在高位,高位字节存放在低位。
  2. 小端序(Little Endia):低位字节存放在低位,高位字节存放在高位。

 为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)。

C语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:

uint16_t htons(uint16_t hostshort);   // uint16_t  2字节的整数 unsigned short

uint32_t htonl(uint32_t hostlong);    // uint32_t  4字节的整数 unsigned int

uint16_t ntohs(uint16_t netshort);

uint32_t ntohl(uint32_t netlong);

h    host(主机);

to  转换;

n    network(网络);

s    short(2字节,16位的整数);

l     long(4字节,32位的整数)

 在计算机中,IPv4的地址用4字节的整数存放,通讯端口用2字节的整数(0-65535)存放。

 四、网络通讯中的各种结构体

 1、sockaddr结构体

存放协议族、端口和地址信息,客户端的connect()和服务端的bind()需要这个结构体

struct sockaddr {

  unsigned short sa_family;     // 协议族,与socket()函数的第一个参数相同,填AF_INET。

  unsigned char sa_data[14];   // 14字节的端口和地址。

};

 2、sockaddr_in结构体

sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便。

所以定义了等价的sockaddr_in结构体,它的大小与sockaddr相同,可以强制转换成sockaddr

 struct sockaddr_in { 

  unsigned short sin_family;    // 协议族,与socket()函数的第一个参数相同,填AF_INET。

  unsigned short sin_port;       // 16位端口号,大端序。用htons(整数的端口)转换。

  struct in_addr sin_addr;        // IP地址的结构体。192.168.101.138

  unsigned char sin_zero[8];    // 未使用,为了保持与struct sockaddr一样的长度而添加。

};

struct in_addr {                  // IP地址的结构体。

  unsigned int s_addr;       // 32位的IP地址,大端序。

};

 3、gethostbyname函数(域名/主机名/字符串IP转大端序

 根据域名/主机名/字符串IP获取大端序IP,用于网络通讯的客户端程序中。

struct hostent *gethostbyname(const char *name);

struct hostent {

  char *h_name;       // 主机名。

  char **h_aliases;    // 主机所有别名构成的字符串数组,同一IP可绑定多个域名。

  short h_addrtype;    // 主机IP地址的类型,例如IPV4(AF_INET)还是IPV6。

  short h_length;     // 主机IP地址长度,IPV4地址为4,IPV6地址则为16。

  char **h_addr_list;    // 主机的ip地址,以网络字节序存储。

};

#define h_addr h_addr_list[0] // for backward compatibility.

 转换后,用以下代码把大端序的地址复制到sockaddr_in结构体的sin_addr成员中。

memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);

四、字符串IP与大端序IP的转换(只能字符串IP转大端序)

C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换,用于网络通讯的服务端程序中。

  • typedef unsigned int in_addr_t;    // 32位大端序的IP地址。

  • // 把字符串格式的IP转换成大端序的IP,转换后的IP赋给sockaddr_in.in_addr.s_addr。in_addr_t inet_addr(const char *cp);

  • // 把字符串格式的IP转换成大端序的IP,转换后的IP将填充到sockaddr_in.in_addr成员。

int inet_aton(const char *cp, struct in_addr *inp); 

  • // 把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址。char *inet_ntoa(struct in_addr in);

五、socket通信的服务端类

头文件.h

class ctcpserver
{
private:
    int m_socklen; //结构体struct sockaddr_in的大小
    struct sockaddr_in m_clientaddr; //客户端的地址信息
    struct sockaddr_in m_servaddr;  //服务端的地址信息
    int m_listenfd;  //服务端用于监听的socket
    int m_connfd;  //客户端连接上来的socket
public:
    ctcpserver():m_listenfd(-1),m_connfd(-1){} //构造函数
  
    // 服务端初始化。
    // port:指定服务端用于监听的端口。
    //backlog:设置监听sock的第二个参数。
    // 返回值:true-成功;false-失败,一般情况下,只要port设置正确,没有被占用,初始化都会成功。
    bool initserver(const unsigned int port,const int backlog=5);

    // 从已连接队列中获取一个客户端连接,如果已连接队列为空,将阻塞等待。
    // 返回值:true-成功的获取了一个客户端连接,false-失败,如果accept失败,可以重新accept。
    bool accept();

    //获取客户端的ip地址
    // 返回值:客户端的ip地址,如"192.168.1.100"。
    char *getip();

    // 接收对端发送过来的数据。
    // buffer:存放接收数据的缓冲区。
    // ibuflen: 打算接收数据的大小。
    // itimeout:等待数据的超时时间(秒):-1-不等待;0-无限等待;>0-等待的秒数。
    // 返回值:true-成功;false-失败,失败有两种情况:1)等待超时;2)socket连接已不可用。
    bool read(string &buffer, const int itimeout=0);
    bool read(Void *buffer,const int ibuflen,const int itimeout=0);

    // 向对端发送数据。
    // buffer:待发送数据缓冲区。
    // ibuflen:待发送数据的大小。
    // 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。
    bool write(const string &buffer);                          // 发送文本数据。
    bool write(const void *buffer,const int ibuflen);   // 发送二进制数据。

    //关闭监听的socket,常用于多进程服务程序的子进程代码中。
    void closelisten();

    //关闭客户端的socket,常用于多进程服务程序的父进程代码中。
    void closeclient();

    ~ctcpserver();   // 析构函数自动关闭socket,释放资源。
}

服务端初始化函数,连接客户端函数,获得客户端ip函数

// 服务端初始化。
// port:指定服务端用于监听的端口。
//backlog:设置监听sock的第二个参数。
// 返回值:true-成功;false-失败,一般情况下,只要port设置正确,没有被占用,初始化都会成功。
bool ctcpserver::initserver(const unsigned int port,const int backlog)
{
    // 如果服务端的socket>0,关掉它
    if(m_listenfd > 0){ ::close(m_listenfd); m_listenfd=-1;}
    
    //创建监听的socket
    if((m_listenfd = socket(AF_INET,SOCK_STREAM,0))<=0) return false;

    // 忽略SIGPIPE信号,防止程序异常退出。
    // 如果往已关闭的socket继续写数据,会产生SIGPIPE信号,它的缺省行为是终止程序,所以要忽略它。
    signal(SIGPIPE,SIG_IGN); 

    // 打开SO_REUSEADDR选项,当服务端连接处于TIME_WAIT状态时可以再次启动服务器,
    // 否则bind()可能会不成功,报:Address already in use。
    int opt = 1; 
    setsockopt(m_listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    memset(&m_servaddr,0,sizeof(m_servaddr));
    m_servaddr.sin_family = AF_INET;
    m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任意IP地址
    m_servaddr.sin_port = htons(port);
    if(bind(m_listenfd,(struct sockaddr *)&m_servaddr,sizeof(m_servaddr)) != 0){
        closelisten();return false;
    }
    if(listen(m_listenfd,backlog) != 0 ){
        closelisten();return false;
    }
    return true;
}
 
// 从已连接队列中获取一个客户端连接,如果已连接队列为空,将阻塞等待。
// 返回值:true-成功的获取了一个客户端连接,false-失败,如果accept失败,可以重新accept。 
bool ctcpserver::accept()
{
    if(m_listenfd==-1) return false;
    
    int m_socklen = sizeof(struct sockaddr_in);
    if(m_connfd = ::accept(m_listenfd, (struct sockaddr *)&m_clientaddr,(socklen_t *)&m_socklen)) < 0)
        return false;
    return true;
}
 
// 获取客户端的ip地址。
// 返回值:客户端的ip地址,如"192.168.1.100"。
char *ctcpserver::getip()
{
    return(inet_ntoa(m_clientaddr.sin_addr));
}

收发数据

收发数据设置超时时间,采用poll的超时机制。

tcp的发送和接收:

  • 发送:把数据放到tcp的发送缓冲区。
  • 接收:从tcp接收缓冲区中取数据。

分包和粘包:

  • 分包:tcp报文的大小缺省是1460 字节,如果发送缓冲区中的数据超过1460字节,tcp将拆分成多个包发送,如果接收方及时的从接收缓冲区中取走了数据,看上去像就接收到了多个报文
  • 粘包:tcp接收到数据之后,有序放在接收缓冲区中,数据之间不存在分隔符的说法,如果接收方没有及时的从缓冲区中取走数据看上去就象粘在了一起

同时为了避免出现TCP分包和粘包现象,发送文本数据时,使用报文长度(四字节整数)+报文内容来区分报文,eg:Hello world,先接收11,在接收Hello world。如果使用原生的send和recv函数,将发生分包和粘包的现象

发送数据

首先封装了原生send()函数为writen()函数

//send()函数:send函数的功能是把待发送的数据拷贝到发送缓冲区。
//          返回值是已拷贝的字节数,正常情况下,与待发送数据的字节数相同
//          如果发送缓冲区的空间不足,则返回本次已拷贝的字节数
//为了保证全部的数据被发送,应该循环调用send函数,直到全部的数据被发送完成。

// 向已经准备好的socket中写入数据。
// sockfd:已经准备好的socket连接。
// buffer:待发送数据缓冲区的地址。
// n:待发送数据的字节数。
// 返回值:成功发送完n字节的数据后返回true,socket连接不可用返回false。
bool writen(const int sockfd,const char *buffer,const size_t n)
{
    int nleft=n; //剩余需要写入的字节数
    int idx=0; //已经写入的字节数
    int nwritten; //每次调用send()函数写入的字节数
    
    while(nleft > 0){
        if((nwritten = send(sockfd, buffer+idex, nleft,0)) <= 0) return false;
        nleft -= nwritten;
        idx += nwritten;  
    }

    return true;
}

发送数据代码:

// 发送文本数据。
bool ctcpserver::write(const string &buffer)
{
    if (m_connfd==-1) return false;

    return(tcpwrite(m_connfd,buffer));
}

// 发送二进制数据。
bool ctcpserver::write(const void *buffer,const int ibuflen)
{
    if (m_connfd==-1) return false;

    return(tcpwrite(m_connfd,(char*)buffer,ibuflen));
}


// 向socket的对端发送数据。
// sockfd:可用的socket连接。
// buffer:待发送数据缓冲区的地址。
// ibuflen:待发送数据的字节数。
// 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。
bool tcpwrite(const int sockfd,const string &buffer);                             // 写入文本数据。
bool tcpwrite(const int sockfd,const void *buffer,const int ibuflen);      // 写入二进制数据。


// 发送文本数据。
bool tcpwrite(const int sockfd,const string &buffer)      
{
    if (sockfd==-1) return false;

    int buflen=buffer.size();

    //先发送报头
    if(writen(sockfd, (char*)&buflen,4)==false)return false;

    //再发送报文体
    if(writen(sockfd, buffer.c_str(),buflen)==false)return false;
    
    return true;
}

bool tcpwrite(const int sockfd,const void *buffer,const int ibuflen)        // 发送二进制数据。
{
    if (sockfd==-1) return false;

    if (writen(sockfd,(char*)buffer,ibuflen) == false) return false;

    return true;
}

接收数据

首先封装了原生recv()函数为readn()函数:

// 从已经准备好的socket中读取数据。
// sockfd:已经准备好的socket连接。
// buffer:接收数据缓冲区的地址。
// n:本次接收数据的字节数。
// 返回值:成功接收到n字节的数据后返回true,socket连接不可用返回false。
bool readn(const int sockfd,char *buffer,const size_t n)
{
    int nleft=n;    // 剩余需要读取的字节数。
    int idx=0;       // 已成功读取的字节数。
    int nread;       // 每次调用recv()函数读到的字节数。

    while(nleft > 0)
    {
        if ( (nread=recv(sockfd,buffer+idx,nleft,0)) <= 0) return false;

        idx=idx+nread;
        nleft=nleft-nread;
    }

    return true;
}

接收数据代码:

// 接收文本数据。
bool ctcpserver::read(string &buffer,const int itimeout)
{
    if (m_connfd==-1) return false;

    return(tcpread(m_connfd,buffer,itimeout));
}

// 接收二进制数据。
bool ctcpserver::read(void *buffer,const int ibuflen,const int itimeout)
{
    if (m_connfd==-1) return false;

    return(tcpread(m_connfd,buffer,ibuflen,itimeout));
}

// 接收socket的对端发送过来的数据。
// sockfd:可用的socket连接。
// buffer:接收数据缓冲区的地址。
// ibuflen:本次成功接收数据的字节数。
// itimeout:读取数据超时的时间,单位:秒,-1-不等待;0-无限等待;>0-等待的秒数。
// 返回值:true-成功;false-失败,失败有两种情况:1)等待超时;2)socket连接已不可用。
bool tcpread(const int sockfd,string &buffer,const int itimeout=0);// 读取文本数据。
bool tcpread(const int sockfd,void *buffer,const int ibuflen,const int itimeout=0); // 读取二进制数据。


// 接收文本数据。
bool tcpread(const int sockfd,string &buffer,const int itimeout)    // 接收文本数据。
{
    if(socket==-1) return false;

    //如果itimeout>0, 表示等待itimeout秒,如果itimeout秒后接收缓冲区中还没有数据,返回false
    if(itimeout>0){
        //利用了poll的超时机制,poll会阻塞等待sockfd接收缓冲区事件,接收缓冲区有数据了,则唤醒sockfd读数据
        struct pollfd fds; 
        fds.fd=sockfd;
        fds.events=POLLIN;
        if( poll(&fds, 1, itimeout*1000) <= 0) return false;
    }

    // 如果itimeout==-1,表示不等待,立即判断socket的接收缓冲区中是否有数据,如果没有,返回false。
    if (itimeout==-1)
    {
        struct pollfd fds;
        fds.fd=sockfd;
        fds.events=POLLIN;
        if ( poll(&fds,1,0) <= 0 ) return false;
    }
    
    //下面处理避免粘包
    int buflen=0;
    //先读取报文长度,4个字节
    if(readn(sockfd,(char*)&buflen,4)==false)return false;
    // 设置buffer的大小。
    buffer.resize(buflen);
    // 再读取报文内容。
    if(readn(sockfd,&buffer[0],buflen) == false) return false;
    return;
}


// 接收二进制数据。
bool tcpread(const int sockfd,void *buffer,const int ibuflen,const int itimeout)
{
    if (sockfd==-1) return false;

    // 如果itimeout>0,表示需要等待itimeout秒,如果itimeout秒后还没有数据到达,返回false。
    if (itimeout>0)
    {
        struct pollfd fds;
        fds.fd=sockfd;
        fds.events=POLLIN;
        if ( poll(&fds,1,itimeout*1000) <= 0 ) return false;
    }

    // 如果itimeout==-1,表示不等待,立即判断socket的缓冲区中是否有数据,如果没有,返回false。
    if (itimeout==-1)
    {
        struct pollfd fds;
        fds.fd=sockfd;
        fds.events=POLLIN;
        if ( poll(&fds,1,0) <= 0 ) return false;
    }

    // 读取报文内容。
    if (readn(sockfd,(char*)buffer,ibuflen) == false) return false;

    return true;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值