C++ socket编程小结


C++ socket编程小结

#随手笔记
最近做毕设的时候涉及到了这方面的知识,也是经过多次遇坑,趁着还有印象记录一下。本篇主要涉及Linux/unix(MAC OS)下的socket编程,win环境下的socket略有不同。

socket简介

socket英文直译为“插座,插孔”,可能意思是网络编程就是一个封闭的系统上和外部交互的那个“插孔”吧。但是中文翻译为套接字,感觉不是非常好理解,初学的时候甚至因此产生了一些误解。简而言之,socket就是进程之间进行通信的一种约定,或者说是在程序中对TCP和UDP协议的一种封装。

一个最简单的socket示例

先看完整代码:

  • 发送端sender.cpp:
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    int main(){
      int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      struct sockaddr_in serv_addr;
      memset(&serv_addr, 0, sizeof(serv_addr));
      serv_addr.sin_family = AF_INET;
      serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
      serv_addr.sin_port = htons(1234);
      bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr));
      listen(serv_sock, 20);
      struct sockaddr_in clnt_addr;
      socklen_t clnt_addr_size = sizeof(clnt_addr);
      int clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
      char str[] = "hello World!";
      send(clnt_sock, str, sizeof(str), 0);
      close(clnt_sock);
      close(serv_sock);
    }

int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
首先通过socket函数定义了一个本地的socket:serv_sock。由于在Linux系统中万物皆文件,socket也不例外,socket函数返回值就是创建好的socket的文件描述符,是一个整数。后面我们就通过这个文件描述符来操作serv _sock这个socket。
然后看socket函数的参数:AF_INET表示使用ipv4地址;SOCK _STREAM表示使用面向连接的数据传输方式;IPPROTO _TCP表示使用TCP协议。同理对于UDP协议也有相应的参数。
但是现在我们只有一个socket,并不知道他的地址,端口号,也就没法和外间进行连接。在C++中专门有一个数据结构来存储socket的各个属性值,看如下代码段:

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(1234);

sockaddr_ in 便是这个数据结构,其中定义了socket使用的协议,地址和端口号。
单独定义这些也不够,这只是一条数据,需要和一个特定的socket绑定起来才能构成一个完整的socket。在C++中使用bind()函数进行绑定操作。
bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
到这就构成了一个完整的socket。然后使用listen()函数使其进入监听状态,就可以被别的socket连接了。listen(serv_sock, 20);第一个参数指定了哪个socket进入监听状态;第二个参数设定了socket连接请求队列的长度。但是listen函数并不是真正的和某个socket进行了连接,它只是将socket的状态进行了转变。
socket()函数创建的socket默认处于“主动连接”状态,并不会接收别的socket的连接请求。为了能够接收别的进程发来的请求,就需要单独指定一下这个socket的状态,即listen()函数的功能。
然后我们要接受其他进程的连接请求,我们需要创建一个socket作为这个链接的载体。就好比是你在网络中设置了一块共享硬盘,然后将其映射到本地的过程。这个新创建的socket就是你给这个网络硬盘分配的盘号(文件名,挂载点),发起网络连接的进程就相当于网络硬盘。接下来执行真正的连接操作。

    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);

首先是定义一个地址结构,用于接收发起请求的socket的属性信息。然后通过accept函数接收实际的请求,返回值是接收到的“socket”的文件描述符。accept()函数第一个参数是接收连接请求的socket的文件描述符,即要将请求连接到哪个socket上。需要注意的是 程序一旦运行到accpet()函数,就会进入阻塞状态 , 直到处理了一个连接请求程序才会继续。

    char str[] = "hello World!";
    send(clnt_sock, str, sizeof(str), 0);
    close(clnt_sock);
    close(serv_sock);

建立连接之后,通过send()函数发送消息即可。第一个参数为目的socket的文件描述符;第二个参数为要发送的消息,即消息存放的地址;第三个参数为要发送的数据的大小;第四个参数为flag,用于设置一些特殊的操作,一般设置为0;返回值为实际发送的数据的大小,如果发送失败返回-1。
需要注意的是,send()函数只是把要发送的数据存放到发送缓冲区,最终的发送是由协议完成的。因此如果数据过大就可能存在一次发送不完的情况,需要单独处理一下。
最后,使用close()函数将socket关闭即可。

  • 接收端receiver.cpp
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    int main(){
      int sock = socket(AF_INET, SOCK_STREAM, 0);
      struct sockaddr_in serv_addr;
      memset(&serv_addr, 0, sizeof(serv_addr));
      serv_addr.sin_family = AF_INET; 
      serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); 
      serv_addr.sin_port = htons(1234); 
      connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
      char buffer[40];
      recv(sock, buffer, sizeof(buffer)-1, 0);
      printf("Message form server: %s\n", buffer);
      close(sock);
      return 0;
    }

接收端和发送端比较类似,这里就挑不同的地方来说。
首先是接收端作为发起连接的一方,不需要设置listen()函数,socket默认就是主动连接状态。在确定了要连接的socket的ip和端口号之后,通过connect()进行连接即可。这里不需要为socket绑定一个端口号,因为用户并不会关心哪个端口和服务器建立了连接,系统会自动进行分配,只需要知道目的地址和端口就可以达到连接的目的。简单来说就是如果需要在连接建立之前,知道具体的端口号的话就需要使用bind()函数确定一下。如果一定要指定一个端口号也是可以的。
如果要读取消息,使用recv()函数即可。当然在Linux系统中,socket既然作为文件,当然也可以使用write/read函数了。当然这里由于协议的现实,如果数据过大可能不能一次性接收完全,就需要单独处理。

几个小坑

  1. 最开始在做的时候,为了测试send函数的发送机制,尝试了连续两次send消息“hello world”然后使用一个recv接收,但是一直只能收到一个,误以为一个recv只能接收一个send发出的消息。但是这个结论和网上所有对于socket的讲解都不符合,也和我自己对网络协议的认知不符合,最后多次调试才发现原来是c++字符串中的结束字符的原因。recv实际上是收到了两个send发出的消息,但是在printf的时候,遇到了\0’就自动停止了,后面的消息没有被打印出来。所以在发送字符串的时候只需要发送实际的字符数据即可,不需要发送’\0’。
    Server端连续发送两次“hello” receiver接收一次只能显示一个“hello”原因:
    Server每次发送的number为6 即包含了结束字符,实际上receiver接收到了两次“hello”,但是后一次因为第一个“hello”的结束字符而无法被解析出来。
  2. 接着上一个问题,我之所以会发出带有结束符的字符串消息,是因为我在确定消息长度的时候使用了sizeof()函数。但是这个函数实际上返回的是你开辟的空间的大小,不管你是否填充了实际的内容;对于字符串,想要得到实际的字符串长度,需要使用strlen()函数。具体的两个函数的效果演示如果。
67EA50FB-C013-4E70-91CE-428FDA01C44E
  1. 对于发送/接收不完全的数据,需要特别的处理,这里给出两个比较通用的函数实现。
int RecvAll(int &sock, unsigned char *image_recv_data, int image_len) {
  int recv_image_num;
  int remain_size = image_len;
  int point = 0;
  unsigned char image_recv_buff[image_len];
  //分段接收
  while (remain_size > 0) {
    recv_image_num = recv(sock, image_recv_buff, remain_size, 0);
    for (int i = 0; i < recv_image_num; ++i) {
      image_recv_data[point + i] = image_recv_buff[i];
    }
    remain_size = remain_size - recv_image_num;
    point += recv_image_num;
    printf("recv %d bytes data, remain %d bytes data\n", recv_image_num,
           remain_size);
  }
  return point;
}
int SendAll(int &sock, unsigned char *data_sent, int data_len) {
  int sent_data_num;
  int remain_size = data_len;
  int point = 0;
  while (remain_size > 0) {
    sent_data_num = send(sock, data_sent, data_len, 0);
    remain_size = remain_size - sent_data_num;
    point += sent_data_num;
    printf("recv %d bytes data, remain %d bytes data\n", sent_data_num,
           remain_size);
  }
  return point;
}

评论
 上一篇
ROS入门之核心概念和系统安装 ROS入门之核心概念和系统安装
ROS入门之核心概念和系统安装ROS简介ROS(robot operating system)是一个开源的机器人操作系统,最早起源于2007年斯坦福大学willow garage的个人项目。ROS提供机器人硬件抽象描述,底层驱动,节点间的消
2021-07-04
下一篇 
1010 Radix 1010 Radix
1010 Radix题目Given a pair of positive integers, for example, 6 and 110, can this equation 6 = 110 be true? The answer is
2020-05-23
  目录