因为工作需要,需要将我们计算的某种Pose数据发送给远端的client,远端的client规定了Pose的格式。
搭建TCP server的函数大致如下:
///jin
void createTCPServerClient(){
// 创建address
// 结构体socketaddr_in用于描述Internet socket address,而且是ipv4的,区别于socketaddr_in6
struct sockaddr_in server_add;
memset(&server_add, '0', sizeof(sockaddr_in));
char serv_add[] = "192.168.71.40";
server_add.sin_family = AF_INET;//代表使用ipv4进行通信,一般不用作修改
/* Convert Internet host address from numbers-and-dots notation in CP
into binary data in network byte order. */
// 把字符串型的ip地址转换成in_addr_t(实际上还是uint32_t),只不过网络字节序
server_add.sin_addr.s_addr = inet_addr(serv_add);
// htons是将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
// s代表short
server_add.sin_port = htons(9012);
// 创建socket,代表了一个硬件的端口吧,参数0代表协议一般不做修改
// 返回该socket的文件描述符
serverfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(serverfd_ < 0){
LOG(INFO) << "Failed to create socket!";
return;
}else{
LOG(INFO) << "Socket is created!";
// 获取address的地址,强制转换成sockaddr*类型,并与socket绑定
if(bind(serverfd_, (struct sockaddr*)&server_add, sizeof(server_add)) < 0){
LOG(INFO) << "Failed to bind!";
return;
} else{
LOG(INFO) << "Bind successful!";
// 使得socket进入监听模式,开始接收client的连接请求,所有连接请求会进入一个队列
// 这也标志着这个socket是一个server,而不是client
if(listen(serverfd_, 10)){
LOG(INFO) << "Failed to listen!";
return;
}else{
LOG(INFO) << "Start listening...";
}
}
}
struct sockaddr_in client_add;
memset(&client_add, 0, sizeof(sockaddr_in));
uint32_t size = sizeof(sockaddr_in);
while(true){
// isConnected_用于在server这一端标记连接状态
LOG(INFO) << "Connect status in while: " << isConnected_;
if(!isConnected_){
LOG(INFO) << "Waiting for accept: ";
// 使得socket接受队列中的连接请求,后面两个参数用于保存client的地址,
connectionfd_ = accept(serverfd_, (struct sockaddr*)&client_add, (socklen_t*)&size);//第2/3个参数用来返回对方的套接字和地址
// 返回client的文件描述符
if(connectionfd_ < 0){
LOG(INFO) << "Failed to accept!";
}else{
LOG(INFO) << "Connected with connectionfd_ : " << connectionfd_;
// 这里是非常有必要的,因为有两处同时对变量进行修改
isConnectedMutex_.lock();
isConnected_ = true;
isConnectedMutex_.unlock();
}
}
sleep(1);
}
}
其中的while循环是为了保证断线重连,建议放在独立的一个线程中进行:
tcp_server_thread_ = std::thread(std::bind(&PoseManagerModule::createTCPServerClient, this));//创建新线程,用于等待client连接
我这里是为了发送odometry数据,而且是自定义了格式:
odom_count_++;// 无论连接与否都计数
// 判断是否已经连接
if(isConnected_){
SensorOdomMsg bosh_odom_msg;
bosh_odom_msg.timestamp = odo_timestamp * double(1e-6);// us->s
bosh_odom_msg.odomNumber = odom_count_;
bosh_odom_msg.x = p.x();
bosh_odom_msg.y = p.y();
bosh_odom_msg.yaw = p.yaw();
bosh_odom_msg.velocitySet = false;
// 往client进行写入操作,connectionfd_是server端对client的文件描述符
// 后面的两个参数是写入的字符的起始地址和总字节数,会严格按照struct定义的成员变量的顺序进行以字符串的形式发送
// 如果写入字节数<=0,说明写入不成功,已经断开了,需要重新连接
if(write(connectionfd_, &bosh_odom_msg, sizeof(bosh_odom_msg)) <= 0){
LOG(INFO) << "Write error!";
// 关闭这个文件描述符(socket)
close(connectionfd_);
isConnectedMutex_.lock();
// 置为false,上面的断线重连起作用
isConnected_ = false;
isConnectedMutex_.unlock();
LOG(INFO) << "isConnected status: " << isConnected_;
}
if(odom_count_%1000 == 0){
LOG(INFO) << "Sequence " << odom_count_ << " time : " << bosh_odom_msg.timestamp;
}
}
其中,SensorOdomMsg的定义如下:
// __attribute__((packed))的作用是不对齐,因为在传输的过程中。某个成员变量如果用三个字节就可以表示,但是为了对齐会用四个字节,这里强制不对齐,更加紧凑,是与client端的解析对应的
struct __attribute__((packed)) SensorOdomMsg{
double timestamp{0.};
uint32_t odomNumber{0};
uint64_t epoch{0};
double x{0.};
double y{0.};
double yaw{0.};
double v_x{0.};
double v_y{0.};
double omega{0.};
bool velocitySet{false};
};
最后,再举例说一下server该怎么向上面的client发起连接请求并接收和解析数据:
#include <sys/socket.h>
//#include <sys/types.h>
#include <netinet/in.h>
//#include <netdb.h>
#include <stdio.h>
#include <string.h>
//#include <stdlib.h>
#include <unistd.h>
//#include <errno.h>
//#include <arpa/inet.h>
//#define SEND_BUF_SIZE 1024
int main(int argc, char *argv[])
{
// if (argc != 2)
// {
// printf("\n Usage: %s <ip of server> \n",argv[0]);
// return 1;
// }
char recvBuff[70] = {0};
int sockfd = 0, n = 0;
// 创建socket,返回文件描述符
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("\n Error : Could not create socket \n");
return 1;
}
struct sockaddr_in serv_addr;
memset(&serv_addr, '0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(5000);
//将 ip地址 转换
// char tmp[] = "192.168.71.40";
// if (inet_pton(AF_INET, tmp, &serv_addr.sin_addr) <= 0)//presentation to net in AF_INET
// {
// printf("\n inet_pton error occured\n");
// return 1;
// }
// 用inet_addr()、inet_pton,或者下面这种htonl都是可以的,建议使用inet_addr()比较好理解吧
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 向server申请建立连接,sockfd为本地client的描述符,后面两个参数为client的地址,通过地址建立连接
// 猜测,返回值估计是server的文件描述符,这里不需要写,
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("\n Error : Connect Failed \n");
return 1;
}
//连上之后,就可以通信了
// char sendBuff[SEND_BUF_SIZE] = {0};
// const char *src = "Client====";
// strncpy(sendBuff, src, SEND_BUF_SIZE - 1);
// sendBuff[SEND_BUF_SIZE - 1] = '\0';
// for (int i = 0; i < 10; ++i) {
// write(sockfd, sendBuff, strlen(sendBuff));
// }
int count = 0;
while (count++ < 5)
{
// 从client的文件描述符读取数据
if ((n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) > 0) {
printf("%d\n", n);
recvBuff[n] = 0;//ascii码的0代表空字符
// printf("%s\n", recvBuff);
// 把字符指针强制转换成double型,再打印显示
printf("%f\n", *(reinterpret_cast<double*>(&recvBuff[36])));
}
}
// 关闭client本地的socket,对方再write就不成功
close(sockfd);
if(n < 0)
{
printf("\n Read error \n");
}
return 0;
}