C語言、嵌入式應用:TCP通信實踐


關於socket的筆記,之前已經有分享過兩篇相關的文章:

【socket筆記】TCP、UDP通信總結

【socket應用】基於C語言的天氣客戶端的實現

本篇筆記我們再來一起回顧一下socket相關的知識:我們的開發板作為TCP客戶端,與TCP服務端程序進行通信

准備相關工程

  • 硬件:小熊派開發板。
  • 軟件:STM32+RT-Thread
  • 開發工具:RT-Thread Studio V1.1.0。

實驗前提是我們的開發板與我們的PC所處的網絡環境在同一網段內。

我們的開發板聯網模塊時ESP8266。這里需要使用RTT的at_device軟件包,這在之前的筆記中已經有介紹:【RT-Thread筆記】Onenet軟件包的使用


RT-Thread的網絡框架

在編寫代碼之前有必要先了解一下RT-Thread的網絡框架結構(圖片來源:RT-Thread官網):

從下往上看:

第 1 層:與硬件相關的一些網絡模塊,這里我們用的是ESP8266

第 2~4 層:一些中間層。本次實驗中我們可以不用深究,我們把這幾層看做一個黑盒子,先不用管里面的實現。有精力的朋友可以去研究,初學朋友暫時先別去碰,碰就是勸退。。。不過也可以稍微了解一些這幾層是什么。

第 2 層是協議棧層。這些是一些輕量型的、用於嵌入式中的TCP/IP 協議棧 。

第 3 層是網卡層。通過 netdev 網卡層用戶可以統一管理各個網卡信息和網絡連接狀態,並且可以使用統一的網卡調試命令接口。

第 4 層是SAL 套接字抽象層。通過它 RT-Thread 系統能夠適配下層不同的網絡協議棧,並提供給上層統一的網絡編程接口,方便不同協議棧的接入。

第 5 層應用層標准socket接口。其提供一套標准 BSD Socket API。所謂標准就是我們在RT-Thread應用編程中用的網絡接口與在PC上進行網絡編程所用的接口函數是一樣的,如:

有了這樣的一套標准 BSD Socket API,我們的程序就可以在 PC 上編寫、調試:

然后再移植相關代碼到 RT-Thread 操作系統上,這給我們提供了很大的便利。

其中,第4層和第5層在在代碼中是用宏來關聯起來的:

更多的關於RT-Thread的網絡框架介紹可以查看官網文檔:

https://www.rt-thread.org/document/site/programming-manual/sal/sal/#

下面開始編寫測試代碼,首先我們需要清楚一個TCP客戶端-服務端模型

編寫代碼

(1)編寫TCP客戶端代碼(開發板代碼)

我們這里編寫的客戶端測試代碼就是按照上面那個圖來一步一步的編寫的:

1、創建一個socket

2、連接服務端

3、發送數據

4、阻塞等待接收數據

5、關閉連接

①創建一個socket

用到的接口:

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

我們創建socket相關的代碼如下:

/* 創建一個socket,類型是SOCKET_STREAM, TCP類型 */
if ((sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
    /* 創建socket失敗 */
    rt_kprintf("Socket error\n");
    return -1;
}

domain / 協議族類型:

  • AF_INET: IPv4
  • AF_INET6: IPv6

type / 協議類型:

  • SOCK_STREAM:流套接字
  • SOCK_DGRAM: 數據報套接字
  • SOCK_RAW: 原始套接字

protocol / 傳輸協議

  • IPPROTO_TCP
  • IPPROTO_UDP
  • ......

②連接服務端

用到的接口:

int connect(int s, const struct sockaddr *name, socklen_t namelen);

我們連接服務端相關的代碼如下:

/* 從終端獲取URL */
url = argv[1];

/* 從終端獲取端口並轉為無符號數據 */
port = strtoul(argv[2], 0, 10);

/* 通過函數入口參數url獲得host地址(如果是域名,會做域名解析) */
host = gethostbyname(url);

/* 初始化預連接的服務端地址 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));

/* 連接到服務端 */
if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
{
    /* 連接失敗 */
    rt_kprintf("Connect fail!\n");
    closesocket(sock_fd);
    return -1;
}
else
{
    /* 連接成功 */
    rt_kprintf(">>>>>>>>>>>>Connect server(%s %d) success!\n", url, port);
}

③發送數據

用到的接口:

int send(int s, const void *dataptr, size_t size, int flags);

我們發送數據相關的代碼如下:

 /* 發送數據 */
 if (send(sock_fd, argv[3], strlen(argv[3]), 0) < 0)
 {
     /* 發送失敗,關閉這個連接 */
     closesocket(sock_fd);
     rt_kprintf("\nsend error,close the socket.\r\n");
 }
 else
 {
     /* 發送成功 */
     rt_kprintf(">>>>>>>>>>>>Send data(%s) to server success!\n", argv[3]);
 }

④接收數據

用到的接口:

int recv(int s, void *mem, size_t len, int flags);

我們接收數據的相關代碼如下:

/* 等待服務端發送過來的數據 */
if (recv(sock_fd, recv_buf, 100, 0) < 0)
{
    /* 接收失敗,關閉這個連接 */
    closesocket(sock_fd);
    rt_kprintf("\nreceived error,close the socket.\r\n");
}
else
{
    /* 接收成功,打印收到的數據 */
    rt_kprintf(">>>>>>>>>>>>Recv data from server: %s\n",recv_buf);
}

⑤關閉連接

用到的接口:

int closesocket(int s);

(2)編寫TCP服務端代碼(PC機)

這里提供的是Windows環境下的TCP服務端程序代碼,編寫思路也是按照上面的TCP客戶端-服務端模型來的,相關接口就不詳細列舉了,直接貼代碼吧:

/*	程序:Windows環境下的TCP服務端程序
	gcc編譯命令:gcc tcp_server.c -lwsock32 -o tcp_server.exe
	
	微信公眾號:嵌入式大雜燴
	作者:ZhengN
*/

#include <stdio.h>
#include <winsock2.h>

#define BUF_LEN  100

int main(void)
{
	WSADATA wd;
	SOCKET ServerSock, ClientSock;
	char Buf[BUF_LEN] = {0};
	SOCKADDR ClientAddr;
	SOCKADDR_IN ServerSockAddr;
	int addr_size = 0, recv_len = 0;
	
	/* sock需要 */
	WSAStartup(MAKEWORD(2,2),&wd);  
	
	printf("===============這是一個TCP服務端程序==============\n");
	
	/* 創建服務端socket */
	if (-1 == (ServerSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
	{
		printf("socket error!\n");
		exit(1);
	}
	
	/* 設置服務端信息 */
    memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); 		// 給結構體ServerSockAddr清零
    ServerSockAddr.sin_family = AF_INET;  						// 使用IPv4地址
    ServerSockAddr.sin_addr.s_addr = inet_addr("192.168.1.101");// 本機IP地址
    ServerSockAddr.sin_port = htons(1314);  					// 端口
	
	/* 綁定套接字 */
    if (-1 == bind(ServerSock, (SOCKADDR*)&ServerSockAddr, sizeof(SOCKADDR)))
	{
		printf("bind error!\n");
		exit(1);
	}
		
	/* 進入監聽狀態 */
	if (-1 == listen(ServerSock, 10))
	{
		printf("listen error!\n");
		exit(1);
	}
	
	addr_size = sizeof(SOCKADDR);

	while (1)
	{
		/* 監聽客戶端請求,accept函數返回一個新的套接字,發送和接收都是用這個套接字 */
		if (-1 == (ClientSock = accept(ServerSock, (SOCKADDR*)&ClientAddr, &addr_size)))
		{
			printf("socket error!\n");
			exit(1);
		}

		/* 接受客戶端的返回數據 */
		int recv_len = recv(ClientSock, Buf, BUF_LEN, 0);
		printf("客戶端發送過來的數據為:%s\n", Buf);
		
		/* 發送數據到客戶端 */
		send(ClientSock, Buf, recv_len, 0);
		
		/* 關閉客戶端套接字 */
		closesocket(ClientSock);
		
		/* 清空緩沖區 */
		memset(Buf, 0, BUF_LEN);  
	}

	/*如果有退出循環的條件,這里還需要清除對socket庫的使用*/
	/* 關閉服務端套接字 */
	//closesocket(ServerSock);
    /* WSACleanup();*/

	return 0;
}

驗證、分析

1、PC端自驗證

我們使用我們自己用C語言編寫的客戶端、服務端程序進行驗證:

2、STM32<-->PC

(1)STM32作為客戶端,與PC端我們自己編寫的服務端程序進行通信。

tcp_client命令是我們使用MSH_CMD_EXPORT宏導出的命令,如

MSH_CMD_EXPORT(tcp_client, tcp_client sample);

我們可在終端按下TAB鍵或者輸入help來查看有沒有導出成功:

我們的測試命令格式為:

tcp_client URL PORT DATA

其中,URL 參數代表網址或IP地址,這里是局域網內的TCP通信測試,所以這個參數其實就是我們電腦的IP地址,可以在cmd下輸入ipconfig命令進行查看:

PORT 參數代表端口。這里要輸入的是服務端程序綁定的端口號。端口使用16bit進行編號,即其范圍為: 0~65536

0~1023 的端口一般由系統分配給特定的服務程序,例如 Web 服務的端口號為 80,FTP 服務的端口號為 21等。

我們這里的服務端程序端口號可以設置為1024~65535范圍內的隨意一個數。但要注意的是我們輸入的測試命令中的PORT參數要與服務端程序綁定的端口一樣,否則客戶端就連接不上服務端:

DATA參數代表我們要發送給服務端的數據。

需要注意的是,我們在進行測試時需要先啟動服務端程序。如果服務端程序還未啟動就運行我們的客戶端程序,就會出現連接失敗:

(2)STM32作為客戶端,PC端網絡調試助手作為服務端。

從這個網絡助手中可以看到在收到數據的同時可以顯示出客戶端的IP及端口號。客戶端的端口號是系統隨機分配的(范圍為:1024~65535):

所以我們不關心端口號,但是我們可以查看客戶端的IP地址。如:

除了這個串口調試助手之外,之前也有分享過一個很好用的socket編程調試工具,有興趣的朋友可移步至:《網絡調試助手的使用》進行查看。

(3)Python實現服務端

服務端程序可以用C、C++、Python等語言來編寫,上面我們用的是C語言。這里我們也來過一把Python隱:

Python代碼:

以下Python代碼來自CSDN博客。博客鏈接:

https://blog.csdn.net/liao392781/article/details/80116600?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
#coding=utf-8
#創建TCP服務器
from socket import *
from time import ctime
 
HOST='192.168.1.101' #這個是我的服務器ip,根據情況改動
PORT=1314 #我的端口號
BUFSIZ=1024
ADDR=(HOST,PORT)
 
tcpSerSock=socket(AF_INET,SOCK_STREAM) #創服務器套接字
tcpSerSock.bind(ADDR) #套接字與地址綁定
tcpSerSock.listen(5)  #監聽連接,傳入連接請求的最大數,一般為5就可以了
 
while True:
    print('waiting for connection...')
    tcpCliSock,addr =tcpSerSock.accept()
    print('...connected from:',addr)
 
    while True:
        stock_codes = tcpCliSock.recv(BUFSIZ).decode() #收到的客戶端的數據需要解碼(python3特性)
        print('stock_codes = ',stock_codes)    #傳入參數stock_codes
        if not stock_codes:
            break
        tcpCliSock.send(('[%s] %s' %(ctime(),stock_codes)).encode())  #發送給客戶端的數據需要編碼(python3特性)
        after_close_simulation = tcpCliSock.recv(BUFSIZ).decode() #收到的客戶端的數據需要解碼(python3特性)
        print('after_close_simulation = ',after_close_simulation)    #傳入參數after_close_simulation
        if not after_close_simulation:
            break
        tcpCliSock.send(('[%s] %s' %(ctime(),after_close_simulation)).encode())  #發送給客戶端的數據需要編碼(python3特性) 
 
    tcpCliSock.close()
tcpSerSock.close()

以上就是本次的分享。如果覺得文章不錯,轉發分享、在看,也是我們繼續更新的動力。


我的個人博客:https://www.lizhengnian.cn/

我的微信公眾號:嵌入式大雜燴

我的CSDN博客:https://blog.csdn.net/zhengnianli


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM