前两篇博客实现了旭日x3与ESP32之间的UDP多机通信。正巧我手边有三块旭日X3派,如何实现旭日x3派的多机通讯呢?答案是显而易见的——TCP通信!

TCP和UDP区别:

1.TCP简介

TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为:TCP)是一种面向连接的、可靠的、基于字节流的通信协议
 
TCP把连接作为最基本的抽象单元,每条TCP连接有两个端点,TCP连接的端点即套接字。
套接字socket = (IP地址+端口号)
TCP连接={socket1,socket2}={(IP1:port1),(IP2,port2)}
TCP提供全双工通信。

2.TCP报文

首部中的重要概念
序号:Seq序号,占32位。用于说明当前数据第一个字节在所有数据(整个文件)中的位置
确认号:Ack序号,占32位。用于告诉发送者接下来需要发送的数据序号。
数据偏移:用于说明首部长度。(比如1111说明首部长为15*4字节)
标志位:tcp标志位有6种:

SYN(synchronous发起一个新连接) 
ACK(acknowledgement 确认)
PSH(push传送) 
FIN(finish结束)
RST(reset重置) 
URG(urgent紧急)

窗口:表示发送/接收缓存窗口大小。如下图。

3.TCP建立与断开连接

3.1 TCP的三次握手

三次握手其实就是建立连接的过程。其过程如图:

1)第一次握手:Client将标志位SYN(建立新连接)置为1,随机产生一个值seq=x,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK(确认)都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
3)第三次握手:Client收到确认后,检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1,并将该数据包发送给Server,Server检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

3.2 TCP的四次挥手

1)第一次挥手:客户端向服务器发起请求释放连接的TCP报文,置FIN为1。客户端进入终止等待-1阶段。
2)第二次挥手:服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,服务器端进入CLOSE-WAIT阶段,并向客户端发送一段TCP报文。客户端收到后进入种植等待-2阶段。
 
3)第三次挥手:服务器做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文。。此时服务器进入最后确认阶段。
4)第四次挥手:客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,于是进入时间等待阶段,并向服务器端发送一段报文。注意:第四次挥手后客户端不会立即进入closed阶段,而是等待2MSL再关闭。
上述TCP简介仅仅包含了TCP的特点和基本原理以及连接建立及断开方式,由于篇幅有限这里不再展开。

两块旭日X3间TCP通信demo实现!

TCP把连接作为最基本的抽象单元,每条TCP连接有两个端点, TCP连接的端点即套接字。
套接字socket = (IP地址+端口号)
可以认为, 套接字就是网络编程的接口,是连接网络的一种工具。
本文先介绍linux下套接字相关函数,最后编码实现一个简单的TCP通信demo。
socket() 函数
socket() 函数创建套接字,原型:
#include <sys/socket.h> 
int socket(int domain, int type, int protocol); 

1)domain 是协议族,也就是 IP 地址类型,常用的有 AF_INET(IPv4) 和 AF_INET6(IPv6)
2)type 为数据传输方式,常用的有 SOCK_STREAM (面向连接的数据传输方式)和 SOCK_DGRAM(无连接的数据传输方式)
3)protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议

bind() 函数

bind() 函数.服务器端 将套接字与特定的IP地址和端口绑定。

  int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);  

成功返回0,失败返回-1。
sockfd 要分配地址信息(IP地址和端口号)的套接字文件描述符
myaddr 存有地址信息的结构体变量地址值
addrlen 第二个结构体变量的长度

其中的结构体原型:

struct sockaddr_in 
{ 
  sa_family_t    sin_family;//地址族 
  uint16_t       sin_port;//16位tcp/udp端口号,以网络字节序保存 
  struct in_addr sin_addr;//32位IP地址,以网络字节序保存 
  char           sin_zero[8];//不使用,必需为0, 
                                   为使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员 
};

该结构体的初始化举例:

struct  sockaddr_in   addr; 
char * serv_ip = "211.217.168.13"; //声明IP地址字符串
char * serv_port = "9190";  //声明端口号字符串
memset(&addr, 0, sizeof(addr)); //结构体变量addr的所有成员置零
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化 
addr.sin_port = htons(atoi(serv_port)); //基于字符串的端口号初始化

connect() 函数

connect() 函数由客户端用来建立连接

int connect(int sockfd,  struct sockaddr *serv_addr, int addrlen);

listen()函数

listen()函数让套接字进入被动监听状态。

int listen(int sockfd, int backlog);

accept()函数

accept() 函数当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。

  int accept(int sockfd, struct sockaddr *addr,  socklen_t *addrlen); 

addr 保存发起连接请求的客户端地址信息的变量地址值 .函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符,套接字自动创建并自动与发起连接的客户端建立连接 。

send()与recv()函数

send() 函数发送数据,recv() 函数接受数据

int send(int sockfd,  char *buf, int len, int flags);

int recv(int sockfd, char *buf, int len, int flags);

buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项。

closesocket()函数关闭套接字

服务端旭日X3代码(C/C++):

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>

#define MAXDATASIZE 100  

#define PORT 8087
#define KEY 123
#define SIZE 1024

int main()
{

    char buf[100];
    memset(buf,0,100);

    int server_sockfd,client_sockfd;
    socklen_t server_len,client_len;

    struct  sockaddr_in server_sockaddr,client_sockaddr;

    /*建立服务端套接字*/
    server_sockfd = socket(AF_INET,SOCK_STREAM,0);
    
    server_sockaddr.sin_family = AF_INET;		//ipv4
    server_sockaddr.sin_port = htons(PORT);   //端口 #define PORT 8087
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任意地址

    server_len = sizeof(server_sockaddr);
    
    int on;
    setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR,&on,sizeof(on));
    
    /*bind 将套接字与IP地址和端口绑定*/
    if(bind(server_sockfd, (struct sockaddr*)&server_sockaddr, server_len)==-1){
        printf("bind error");
        exit(1);
    }

	//监听
    if(listen(server_sockfd, 5)==-1){
        printf("listen error");
        exit(1);
    }

    client_len = sizeof(client_sockaddr);

    pid_t ppid,pid;

    while(1) {
	printf("start\n");

		//当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。
        if((client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_sockaddr, &client_len)) == -1){
            printf("connect error");
            exit(1);
        } else 
	{
            printf("create connection successfully\n");
	    while(1){
	    	//发送数据
	    	int error = send(client_sockfd, "I am the server", strlen("I am the server"), 0);
		if(error > 0 ){
		    printf("send    : I am the server\n");
		}
        
        //接收数据
		int numbytes = recv(client_sockfd, buf, MAXDATASIZE, 0);
		if (numbytes > 0) { 
          	   buf[numbytes] = '\0'; 
          	   printf("Received: %s\n",buf);
		 }
	
	   	sleep(1);
	    }
            
        }
	printf("next\n");
        
 } 
    return 0;
}

客户端c/c++代码

#include <stdio.h> 
#include <stdlib.h> 
#include <errno.h> 
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h> 
#include <sys/wait.h> 
#include <unistd.h> 
#include <arpa/inet.h> 

#define SERVER_PORT 8087
#define MAXDATASIZE 100  
#define SERVER_IP "127.0.0.1"//修改为服务端旭日X3派的IP地址

int main() { 
    int sockfd, numbytes; 
    char buf[MAXDATASIZE]; 
    struct sockaddr_in server_addr; 

    printf("\n client initialization \n"); 
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { 
        perror("socket"); 
        exit(1); 
    }

    server_addr.sin_family = AF_INET; 
    server_addr.sin_port = htons(SERVER_PORT); 
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); 
    bzero(&(server_addr.sin_zero),sizeof(server_addr.sin_zero)); 

	//连接
    if (connect(sockfd, (struct sockaddr *)&server_addr,sizeof(struct sockaddr_in)) == -1){
         perror("connect error"); 
         exit(1);
     } 
    
     while(1) { 
         bzero(buf,MAXDATASIZE); 
         //接收消息
         if ((numbytes = recv(sockfd, buf, MAXDATASIZE, 0)) == -1){  
             perror("recv"); 
             exit(1);
         } else if (numbytes > 0) {  //若成功接收
             int len, bytes_sent;
             buf[numbytes] = '\0'; 
            printf("Received:%s\n",buf);
            
            char msg[100];
	   	    strcpy(msg,"i am a client");
            len = strlen(msg); 
            
            //sent to the server
            if(send(sockfd, msg,len,0) == -1){ 
                perror("send error"); 
            }else{
           		 printf("Send    :i am a client\n"); 
            }
        } 
        else { 
            printf("soket end!\n"); 
            break;
        } 
	sleep(1);
    }  
        close(sockfd); 
        return 0;
    }

最后别忘了编译CPP文件

g++ -O3 tcp_client.cpp -o client
g++ -O3 tcp_server.cpp -o server

然后分别在终端运行这两个文件

./server
./client

服务端效果

客户端效果

旭日X3派 TCP通信建立成功!