C++实现的socket基础通讯

一、网络通讯程序

(1)服务端与客户端

网络通讯是指两台计算机中的程序进行传输数据的过程。

客户程序(客户端):指主动发起通讯的程序

服务程序(服务端/服务器):被动的等待,然后为向它发起通讯的客户端提供服务

通讯前,客户端必须提前知道服务端的IP地址和端口,而服务端不需要知道客户端的IP

(2)服务端与客户端基本流程

客户端流程:

①创建客户端的socket

②向服务器发起连接请求

③与服务端通信,发送一个请求报文后等待恢复,然后再发下一个请求报文

④关闭socket,释放资源

服务端流程:

①创建服务端的socket

②把服务端用于通信的IP和端口绑定到socket上

③把socket设置为可连接状态

④受理客户端的连接请求

⑤与客户端通信,接收客户端发过来的报文并恢复

⑥关闭socket,释放资源

二、网络编程的细节

(1)socket函数详解

创建socket:

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

···domain参数  通讯协议族

        PF_INET       IPv4互联网协议族,是最普及最常用的

        PD_INET6     IPv6互联网协议族

···type参数  数据传输类型

        SOCK_STREAM     面向连接的socket:数据不会丢失,数据顺序不会错乱,双向通道

        SOCK_DGRAM      无连接的socket:数据可能会丢失,数据顺序可能错乱,传输效率更高

···protocol参数   最终使用的协议

        在IPv4网络协议家族中,

                SOCK_STREAM的协议只有IPPROTO_TCP

                SOCK_DGRAM的协议只有IPPROTO_UDP

        本参数也可以填0

socket函数成功返回一个有效的socket,失败时返回-1,只要参数没填错,基本不会失败

(2)主机字节序与网络字节序

1)大端和小端

大端序:低位字节存放在高地址,高位字节存放在低地址

小端序:低位字节存放在低地址,高位字节存放在高地址

假如要存入16进制数  0x12345678

大端存储:低地址-> 0x12  0x34  0x56  0x78  ->高地址

小段存储:低地址-> 0x78  0x56  0x34  0x12  ->高地址 

2)网络字节序

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

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

uint16_t htons(uint16_t hostshort);    //2字节(16位)的整数

uint32_t htonl(unit32_t hostlong);  //4字节(32位)的整数

uint16_t ntohs(uint16_t netshort); 

uint32_y ntohl(unit32_t netlong);

h——host即主机        to——转换        n——network即网络        s——short        l——long

3)IP地址和通讯端口

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

例如:        192.168.190.134

        大端:11000000 10101000 10111110 10000110

        小端:10000110 10111110 10101000 11000000

在网络编程中,数据收发的时候有自动转换机制,不需要程序员手动转换

只有向sockaddr_in结构体成员变量填充数据时,才需要考虑字节序的问题

(3)用于存放协议端口和IP地址的结构体

1)sockaddr结构体

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

struct sockaddr{

        unsigned short sa_family; //协议族,固定填AF_INET

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

};

2) sockaddr_in 结构体

        由于操作不方便,定义了等价的sockaddr_in结构体,大小与sockaddr相同,可以强制转换成sockaddr结构体。

struct sockaddr_in{

        unsigned short sin_family;  

        unsigned short sin_port;  //16位端口号,大端序,用htons转换

        struct in_addr sin_addr;  //32位IP地址

        unsigned char sin_zero[8];  //不使用,为了保持与sockaddr长度一致而添加

};

3)in_addr  结构体

        IP地址的结构体

struct in_addr{       

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

};

(4) 字符串IP与大端序IP的转换

C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换

1)把字符串格式IP转换成大端序IP后,赋值给sockaddr_in.in_addr.s_addr

in_addr_t inet_addr(const char *cp);

2)转换后的大端序IP填充到sockaddr_in.in_addr成员

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

3)把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址

char  *inet_ntoa(struct in_addr in);

4)使用gethostbyname()函数,可以把域名/主机名/字符串IP 转换成 结构体

三、封装Socket

(1)将客户端封装成ctcpclient类

class ctcpclient   //TCP通讯的客户端类
{
public:
    int m_clientfd;    //客户端的socket,-1表示未连接或者已断开,>=0表示有效的socket
    string m_ip;        //32位的服务器的ip/域名
    unsigned short m_port;      //16位的通讯端口

    ctcpclient(): m_clientfd(-1) {}        //构造函数
    
    //连接函数,传入两个参数:服务器IP和通讯端口
    //向服务端发起连接请求,成功返回true,失败返回false
    bool connect(string &in_ip,const unsigned short in_port) 
    {
        if(m_clientfd!=-1)    //若socket已连接,直接返回失败
        {    return false;}

        m_ip = in_ip ; m_port = in_port;    //将服务器的IP和端口保存到成员变量中

        //第一步:创建客户端的socket
        if((m_clientfd = socket(AF_INET,SOCK_STREAM,0))==-1)
        {    return false ; }
        //第二步:向服务器发起连接请求
        struct sockaddr_in servaddr;               //定义一个用于存放协议、端口和IP的结构体
        memset(&servaddr , 0 , sizeof(servaddr));  //初始化结构体
        servaddr.sin_family = AF_INET;             //①协议族固定填入AF_INET
        servaddr.sin_port = htons(m_port);     //②指定服务器端的端口

        struct hostent* h;        //定义一个用于存放服务器IP地址的结构体的指针
        if((h = gethostbyname(m_ip.c_str() )) == NULL)
        {
            ::close(m_clientfd);    //关闭客户端socket
            m_clientfd = -1;     //置为-1
            return false;
        }
        memcpy(&servaddr.sin_addr , h->h_addr,h->h_length);  //③指定服务端的IP
    
        if(::connect(m_clientfd , (struct sockaddr *)&servaddr , sizeof(servaddr))==-1)//向服务端发起请求
        {
            ::close(m_clientfd);    //关闭客户端socket
            m_clientfd = -1;     //置为-1
            return false;
        }

        return true;
    }

    //buffer用于存放接收到的报文的内容,maxlen为本次接收报文的最大长度
    bool recv(string &buffer, const size_t maxlen)
    {
        buffer.clear();     //清空容器
        buffer.resize(maxlen);    //设置容器大小为maxlen
        int readn= ::recv(m_clientfd, &buffer[0] , buffer.size() , 0); //操作buffer的内存
        if(readn<=0)    {buffer.clear(); return false; }
        buffer.resize(readn);    //重置buffer的实际大小

        return true;
    }
    
    //向服务端发送报文,成功返回true,失败返回false
    bool send(const string &buffer)
    {
        if(m_clientfd == -1)    return false;    //若socket未连接,直接返回失败
        if((::send(m_clientfd , buffer.data() , buffer.size() , 0)) <=0)    return false;

        return true;
    }
    
    //断开与服务端的连接
    bool close()
    {
        if(m_clientfd==-1)    return false;
        ::close(m_clientfd);
        return true;
    }
    
    ~ctcpclient() { close(); };      //析构函数中调用成员函数close

};

将客户端类封装好后,便可在程序主函数中正常使用客户端的正常功能

int main(int argc,char *argv[])
{
  if (argc!=3)
  {
    cout << "Using:./demo7 服务端的IP 服务端的端口\nExample:./demo7 192.168.101.138 5005\n\n"; 
    return -1;
  }

  ctcpclient tcpclient;
  if (tcpclient.connect(argv[1],atoi(argv[2]))==false)  // 向服务端发起连接请求。
  {
    perror("connect()"); return -1;
  }

  // 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。
  string buffer;
  for (int ii=0;ii<10;ii++)  // 循环3次,将与服务端进行三次通讯。
  {
    buffer="这是第"+to_string(ii+1)+"个用户,编号"+to_string(ii+1)+"。";
    // 向服务端发送请求报文。
    if (tcpclient.send(buffer)==false)
    { 
      perror("send"); break; 
    }
    cout << "发送:" << buffer << endl;

    // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
    if (tcpclient.recv(buffer,1024)==false)
    {
      perror("recv()"); break;
    }
    cout << "接收:" << buffer << endl;

    sleep(1);
  }
}

(2)将服务端封装成ctcpserver类

class ctcpserver         //tcp通讯的服务端类
{
private:
    int m_listenfd;    //用于监听的socket,-1表示未初始化
    int m_clientfd;    //客户端连接上来的socket,-1表示客户端未连接
    string m_clientip;    //客户端的字符串格式IP
    unsigned short m_port;    //服务端用于通讯的端口
public:
    ctcpserver():m_listenfd(-1),m_clientfd(-1){}//创建类对象时将监听socket和通讯socket初始为-1

    //初始化服务器用于监听的socket
    bool initserver(const unsigned short in_port)
    {
        //第一步:创建服务端的socket
        if((m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)    return false;

        m_port=in_port;

        //第二步:把服务器用于通信的IP和端口绑定到socket上
        struct sockaddr_in servaddr;    //定义一个用于存放协议、端口和IP的结构体
        memset(&servaddr,0,sizeof(servaddr));    //初始化结构体
        servaddr.sin_family=AF_INET;    //①协议族固定填入AF_INET
        servaddr.sin_port=htons(m_port);    //②填入服务端的通信端口
        servaddr.sin_addr=htonl(INADDR_ANY);    //③全部IP全都用于通讯

        //绑定服务端的IP和端口
        if(bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
        {
            close(m_listenfd); m_listenfd=-1 ; return false;
        }
        
        //第三步:把socket设置为可连接状态
        if(listen(m_listenfd,5)== -1)
        {
            close(m_listenfd); m_listenfd=-1 ; return false;
        }

        return true;
    }

    //受理客户端的连接
    bool accept()
    {
        struct sockaddr_in caddr;    //客户端的地址信息
        socklen_t addrlen=sizeof(caddr);    //strcut sockaddr_in的大小
        if((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1)
        {    return false;}

        m_clientip=inet_ntoa(caddr.sin_addr);    //客户端的大端IP转换成字符串形式

        return true;
    }

    //获取客户端的字符串形式IP
    const string & clientip() const
    {    return m_clientip;   }

    //向对端发送报文
    bool send(const string &buffer)
    {
        if(m_clientfd == -1)    return false;
        if((::send(m_clientfd , buffer.data(), buffer.size() , 0))<=0)    return false;
        return true;
    }

    //接收对端的报文,buffer用来存放报文的内容
    bool recv(string &buffer , const size_t maxlen)
    {
        buffer.clear();    //清空buffer
        buffer.resize(maxlen);    //设置容器大小
        int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0);    //操作buffer的内存
        if(readn<0) {buffer.clear();return false;}
        buffer.resize(readn);     //重置buffer的实际大小
        
        return true;
    }

    //关闭监听socket
    bool closelisten()
    {
        if(m_listenfd==-1)    return false;
        ::close(m_listenfd);
        m_listenfd=-1;
        return true;
    }
    //关闭通讯socket
    bool closeclient()
    {
        if(m_clientfd==-1)    return false;
        ::close(m_clientfd);
        m_clientfd=-1;
        return true;
    }
    
    ~ctcpserver(){closelisten();closeclient();}
};

将服务端的类封装好后,就可以在程序主函数中调用它的功能

int main(int argc,char *argv[])
{
  if (argc!=2)
  {
    cout << "请输入指定端口"<<endl;
    return -1;
  }

  ctcpserver tcpserver;
  if (tcpserver.initserver(atoi(argv[1]))==false) // 初始化服务端用于监听的socket。
  {
    perror("initserver()"); return -1;
  }

  // 受理客户端的连接  
  // 如果没有已连接的客户端,accept()函数将阻塞等待。
  if (tcpserver.accept()==false)
  {
    perror("accept()"); return -1;
  }
  cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";

  string buffer;
  while (true)
  {
    // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待。
    if (tcpserver.recv(buffer,1024)==false)
    {
      perror("recv()"); break;
    }
    cout << "接收:" << buffer << endl;
 
    buffer="ok";  
    if (tcpserver.send(buffer)==false)  // 向对端发送报文。
    {
      perror("send"); break;
    }
    cout << "发送:" << buffer << endl;
  }
}

 (3)客户端和服务端所需要的头文件

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值