LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

C++网络编程

本文记录使用C++在Linux和Windows下进行Socket通信的方式。

socket是什么?套接字是什么? (biancheng.net)

基本概念

socket提供两种通信机制:

  • stream:流式传输,基于TCP,有序、可靠、双向字节流。
  • datagram:数据报传输,基于UDP,不可靠,可能丢失和乱序。对数据长度有限制,效率高。音视频聊天可以采用。应用场景越来越少。

socket通信流程:

socket客户端服务器通讯

socket()

int socket(int af, int type, int protocol);//Linux
SOCKET socket(int af, int type, int protocol);//Windows
  • af:协议族(Address Family),即IP地址类型。也可以写作PF(Protocol Family),因此所有AF_XX等价于PF_XX
    • AF_INET:IPv4地址,如127.0.0.1。
    • AF_INET6:IPv6地址,如1030::C9B4:FF12:48AA:1A2B。
  • type数据传输方式,即socket类型。
    • SOCK_STREAM:流式,基于TCP
    • SOCK_DGRAM:数据报,基于UDP
  • protocol传输协议
    • 0:根据aftype自动推导。当两种不同协议支持同一种aftype时,无法自动推导。
    • IPPROTO_TCP:TCP传输协议
    • IPPROTO_UDP:UDP传输协议
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);

bind()

将具体的地址和端口绑定到socket

注意绑定前要转为网络字节序

  • htons():host to network short
  • htonl():host to network long
  • ntohl():network to host long
  • ntohs():network to host short
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows
  • sock:由socket()产生的socket描述符。

  • addrlenaddr的大小,由sizeof计算。

  • 返回值:

  • addrsockaddr结构体指针,将sockaddr_in转换得到

    struct sockaddr_in{
        sa_family_t     sin_family;   //地址类型
        uint16_t        sin_port;     //16位的端口号
        struct in_addr  sin_addr;     //32位IP地址
        char            sin_zero[8];  //不使用,一般用0填充
    };
    
    • sin_family:和socket()af取值一致。

    • sin_port:端口号,取值范围为1024~65536,需要使用htons()进行转换。

    • sin_addrin_addr结构体。

      struct in_addr{
          in_addr_t  s_addr;  //32位的IP地址,等价于unsigned long,是一个整数
      };
      //因为s_addr是一个整数,而IP地址一般是字符串,所以要使用inet_addr()进行转换
      unsigned long ip = inet_addr("127.0.0.1");
      

    为什么要转换为sockaddr:

    image-20220325171617582
    • 两者长度都是16B,只是sockaddr将地址和端口合并到一起。要给sa_data赋值必须同时指定,如"127.0.0.1:80",但没有相关函数将字符串转成需要的形式,所以很难给sockaddr赋值,因此使用sockaddr_in代替。
    • sockaddr更通用,而sockaddr_insockaddr_in6分别保存IPv4和IPv6地址。

示例:

sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockaddr_in)); //内存初始化为0
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(PORT);
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
bind(servSock, (sockaddr*)&servAddr, sizeof (servAddr));

connect()

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);  //Windows

用来建立连接,参数和bind()相同,但connect()用于客户端。

listen()

使socket进入被动监听。即没有客户端请求时,socket处于“睡眠”,当接收到请求时,才会“唤醒”。

int listen(int sock, int backlog);  //Linux
int listen(SOCKET sock, int backlog);  //Windows
  • sock:进入监听的socket
  • backlog请求队列的最大长度,如果为SOMAXCONN表示由系统决定,一般比较大。

请求队列

  • socket正在处理客户端请求时,有新的请求进来,就将新请求放入缓冲区,即请求队列。
  • 当请求队列满时,不再接受新请求,Linux的客户端会收到ECONNREFUSED,Windows会收到WSAECONNRESFUSED

accept()

listen()只是监听请求,accept()才真正接受并处理。accept会阻塞程序,直到有新的请求到来。

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);  //Window
  • sock服务器端的socket
  • addr:保存客户端的IP地址和端口号
  • 返回值:一个新的socket,专门用来与该次请求的客户端通信

发送和接收数据

Linux下:

万物皆可为文件,因此socket和普通文件一样,可以直接使用write()/read()发送和接受数据

  • ssize_t write(int fd, const void *buf, size_t nbytes);
    
    • fd:要写入的文件描述符,即socket
    • buf:要写入的数据的地址
    • nbytes:要写入的字节数
    • 返回值:写入成功返回字节数,失败返回-1
  • ssize_t read(int fd, void *buf, size_t nbytes);
    
    • fd:要读取的文件描述符,即socket
    • buf:存放读取出来的数据的地址
    • nbytes:要读取的字节数
    • 返回值:读取成功返回字节数,如果读取到文件结尾返回0,失败返回-1

Windows

需要用专门的send()/recv()

  • int send(SOCKET sock, const char *buf, int len, int flags);
    
    • flags:发送数据时的选项。一般设置为0。
  • int recv(SOCKET sock, char *buf, int len, int flags);
    

socket缓冲区和阻塞模式

缓冲区

每个socket创建后都会有两个缓冲区,输入缓冲区输出缓冲区

write和send并不立即向网络中传输数据,而是先将数据写入输出缓冲区,再通过TCP协议将数据从缓冲区发送到目标机器。一旦数据写入缓冲区,函数就返回,不管发送。

read和recv同理。

缓冲区特性:

  • 在每个TCP socket中单独存在
  • 创建socket时自动生成
  • 关闭socket后TCP仍会发送输出缓冲区的数据
  • 关闭socket后输入缓冲区数据丢失
  • 大小一般为8KB,通过getsockopt获取
  • 发送方在接收到ACK后才会清空输出缓冲区

阻塞模式

对于TCP socket,默认是阻塞的,也可以修改为非阻塞。

使用write和send发送数据时:

  • 首先检查输出缓冲区,如果空间不够则阻塞,直到发送足够数据后空间足够才唤醒
  • 如果TCP正在发送数据,阻塞write和send,直到发送完毕。
  • 如果写入数据大于缓冲区,分批写入。
  • 直到所有数据写入缓冲区,write和send才返回。

使用read和recv读取时:

  • 首先检查输入缓冲区,如果有数据则读取,否则阻塞直到有数据
  • 如果要读取的长度小于缓冲区中数据长度,则剩余数据会积压,直到再次读取。
  • 直到读取到数据后才会返回,否则一直阻塞。

TCP的粘包(数据无边界性)

因为read时缓冲区数据可能是多次write的结果,所以无法区分每一次write的数据边界。例如两次分别写入1和3,读取时会读出13。

img_show