vlambda博客
学习文章列表

物联网lwIP网络开发(三):Socket编程深入

【本期内容概述】

  • 针对Socket接口进行二次封装,内容较为枯燥,但相对简单。在封装中增加了一些容错和处理机制,目的为解决一般化编程下的漏洞,提高程序的普适性。

  • 利用封装后的接口对TCP下的Server端和Client端进行优化,基本可用于项目开发

  • 介绍UDP编程模型,利用封装后的接口进行UDP下Server端的实现


目录:

1、Socket封装

2、TCP Server优化

3、TCP Client优化

4、UDP编程模型

5、UDP Server端实现




—1—

Socket封装

所谓的封装无非就是去判断接口的返回值,并且根据返回值做一定的处理


Socket是一个在Windows或Linux下的接口集,比较丰富

socket_wrap.h

#ifndef _SOCKET_WRAP_H#define _SOCKET_WRAP_H
#include "lwip/sockets.h"
int Socket(int domain, int type, int protocol);
int Bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int Listen(int sockfd, int backlog);
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);int Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int Write(int fd,const void *buf,size_t nbytes);int Read(int fd,void *buf,size_t nbyte);
int Sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);int Recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, socklen_t *fromlen);#endif

socket_wrap.c

#include "socket_wrap.h"#include "FreeRTOS.h"#include "task.h"
/****************socket封装*******************/ * @brief 创建套接字 * @param domain: 协议域 * @param type: 协议类型 * @param protocol: 协议版本 * @retval int: 返回文件描述符,正确大于0 */int Socket(int domain, int type, int protocol){ int fd; fd = socket(domain, type, protocol); //当返回值为-1的时候,基本是lwip的内存不够 //只能将任务删除,用到freetos下的函数vTaskDelete() //参数为NULL时,表示删除任务自身 if(fd < 0){ printf("create socket error\r\n"); //当调用删除任务,就会切换上下文,CPU执行其他任务,这里是不会有返回的 vTaskDelete(NULL); } return fd;}

/****************bind封装*******************/ * @brief 绑定套接字 * @param sockfd: 文件描述符 * @param addr: 绑定的地址信息 * @param addrlen: 地址结构体长度 * @retval int: 返回值大于0,不大于0会自动处理 */int Bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen){ int ret; ret = bind(sockfd, addr, addrlen); if(ret < 0){ printf("bind socket error\r\n"); //当调用删除任务,就会切换上下文,CPU执行其他任务 vTaskDelete(NULL); } return ret;}
/****************listen封装*******************/ * @brief 监听套接字 * @param sockfd: 要监听的文件描述符 * @param backlog: 监听队列的大小,默认为5,unix下为128 * @retval int: 大于0的值 */int Listen(int sockfd, int backlog){ int ret; ret = listen(sockfd, backlog); if(ret < 0){ printf("listen socket error\r\n"); //当调用删除任务,就会切换上下文,CPU执行其他任务 vTaskDelete(NULL); } return ret;
}
/****************accept封装*******************/ * @brief 等待客户端建立好连接 * @param sockfd: 文件描述符 * @param addr: 绑定的地址信息 * @param addrlen: 地址结构体长度---指针类型 * @retval int: 大于0的值 */int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen){
int fd;again: //accept 是阻塞函数,只有客户端连接成功后,才会返回,或者错误返回 fd = accept(sockfd, addr, addrlen); //客戶端连接错误 if(fd < 0){ printf("accept socket error\r\n"); goto again; } return fd;}
/****************connect封装*******************/ * @brief 向目标服务器建立连接 * @param sockfd: 文件描述符 * @param addr: 绑定的地址信息 * @param addrlen: 地址结构体长度---指针 * @retval int: 成功:0,错误小于0 */int Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen){ int ret; ret = connect(sockfd, addr, addrlen); if(ret < 0){ printf("connect socket error\r\n"); //先关闭当前的socket,其实内部是删除这个socket的内存块 //再次连接时应该创建新的socket的内存块,要将旧的删除 close(sockfd); } return ret;}/****************write封装*******************/ * @brief 向套接字发送数据 * @param fd: 文件描述符 * @param buf: 要发送的缓冲区 * @param nbytes: 发送数据的大小,单位为字节 * @retval int: 正确:返回已经发生的数据长度,错误小于0 */int Write(int fd,const void *buf,size_t nbytes){ int ret; ret = write(fd, buf, nbytes); //如果出错,基本上是socket错误了,比如对方socket关闭了 if(ret < 0){ printf("Write socket error\r\n"); //先关闭当前的socket,其实内部是删除这个socket的内存块 close(fd); } return ret;
}/****************read封装*******************/
* @brief 从套接字读取数据 * @param fd: 文件描述符 * @param buf: 要接收的缓冲区 * @param nbytes: 接收数据的大小,单位为字节 * @retval int: 正确:返回已经接收的数据长度,错误小于0,socket关闭等于0 */int Read(int fd,void *buf,size_t nbyte){ int ret; ret = read(fd, buf, nbyte); if(ret == 0){ printf("read socket is close\r\n"); close(fd); }else if(ret < 0){ printf("read socket error\r\n"); close(fd); } return ret;
}
/****************sendto封装*******************/ * @brief 发送数据到指定地址 * @param sockfd: 文件描述符 * @param msg: 要发送的缓冲区 * @param len: 要发送大小 * @param flags: 标志 默认传0 * @param to: 发送的地址信息 * @param tolen: 地址结构体长度 * @retval int: 正确:返回已经发送的数据长度,错误小于0 */int Sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen){ int ret;again: ret = sendto(sockfd, msg, len, flags, to, tolen); if(ret < 0){ printf("sendto socket error\r\n"); goto again; } return ret;
}/****************recvfrom封装*******************/ * @brief 从socket接收数据 * @param sockfd: 文件描述符 * @param buf: 要接收的缓冲区 * @param len: 接收缓冲区的大小 * @param flags: 标志 默认传0 * @param from: 接收到的地址信息 * @param fromlen: 地址结构体大小 * @retval int: 正确:返回已经发送的数据长度,错误小于0 */int Recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, socklen_t *fromlen){ int ret;again: ret = recvfrom(sockfd, buf, len, flags, from, fromlen); if(ret < 0){ printf("recvfrom socket error\r\n"); goto again; } return ret;}


—2—

TCP Server优化


socket_tcp_server.h

#ifndef _SOCKET_TCP_SERVER_H#define _SOCKET_TCP_SERVER_H#define SERVER_IP "192.168.1.178"#define SERVER_PORT 6666#define BUFF_SIZE 4096
void vTcpServerTask(void);


#endif


socket_tcp_server.c

可以很明显得看到优化只是将原来的接口换成了封装之后的接口,注意包含封装过.h的头文件"socket_wrap.h"

#include "socket_tcp_server.h"#include "socket_wrap.h"#include "ctype.h"
char ReadBuff[BUFF_SIZE];
/** * @brief TCP ·þÎñÆ÷ÈÎÎñ * @param None * @retval None */void vTcpServerTask(void){
int sfd, cfd, n, i; struct sockaddr_in server_addr, client_addr; socklen_t client_addr_len;
//创建socket sfd = Socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //绑定socket Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); //监听socket Listen(sfd, 5); //等待客户端连接 client_addr_len = sizeof(client_addr);again: cfd = Accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len); printf("client is connect cfd = %d\r\n",cfd); while(1){ //等待客户端发送数据 n = Read(cfd, ReadBuff, BUFF_SIZE); if(n <= 0){ goto again; } //进行大小写转换 for(i = 0; i < n; i++){ ReadBuff[i] = toupper(ReadBuff[i]); } //写回客户端 n = Write(cfd, ReadBuff, n); if(n < 0){ goto again; } }}


—3—

TCP Client优化


socket_tcp_client.h

#ifndef _SOCKET_TCP_CLIENT_H#define _SOCKET_TCP_CLIENT_H
void vTcpClientTask(void);
#endif


socket_tcp_client.c

#include "socket_tcp_server.h"#include "socket_tcp_client.h"#include "socket_wrap.h"#include "ctype.h"#include "FreeRTOS.h"#include "task.h"
#include "string.h"
static char ReadBuff[BUFF_SIZE];

void vTcpClientTask(void){
int cfd, n, i, ret; struct sockaddr_in server_addr; // int so_reuseaddr_val = 1;again: //创建socket cfd = Socket(AF_UNSPEC, SOCK_STREAM, 0); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); //连接到服务器 //connect其实是一个阻塞接口,内部要完成TCP的三次握手, //当然有超时机制,所以我们需要等待一段时间,才能重新连接到服务器 ret = connect(cfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); if(ret < 0){ //100ms去连接一次服务器,如果没有这个延时,CPU会被一直占用去连接服务器 vTaskDelay(1000); printf("connect fail\r\n"); goto again; } printf("server is connect ok\r\n"); while(1){ //等待服务器发送数据 n = Read(cfd, ReadBuff, BUFF_SIZE); if(n <= 0){ goto again; } //进行大小写转换 for(i = 0; i < n; i++){ ReadBuff[i] = toupper(ReadBuff[i]); } //写回服务器 n = Write(cfd, ReadBuff, n); if(n <= 0){ goto again; } }}


—4—

UDP编程模型

UDP C/S模型


UDP 的API

  • socket

在这里应当注意,UDP下该接口的第二个参数应当是SOCK_DGRAM

SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。

int socket(int domain, int type, int protocol);domain: AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址 AF_INET6 与上面类似,不过是来用IPv6的地址 AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用type: SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。 SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。 SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。 SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议) SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序protocol:0 表示使用默认协议。返回值: 成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno


  • sendto

int sendto(int sockfd, const void *msg,int len, unsigned int flags,  const struct sockaddr *toint tolen);msg: 表示要发送数据的缓冲区len: 发送缓冲区的长度flags: 默认情况下传0即可to: 表示目地机的IP地址和端口号信息,是一个指针类型tolen: 常常被赋值为sizeofstruct sockaddr)。sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1


  • recvfrom

int recvfrom(int sockfd,void *buf,int len, unsigned int flags,struct sockaddr *fromint *fromlen);buf: 接收数据的缓冲区len: 接收缓冲区的长度flags: 默认情况下传0即可from:是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号,是一个传出参数。fromlen: 常置为sizeofstruct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。recvfrom()函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。


—5—

UDP Server端实现


socket_udp_server.h

#ifndef _SOCKET_UDP_SERVER_H#define _SOCKET_UDP_SERVER_H
void vUdpServerTask(void);#endif


socket_udp_server.c

UDP服务端天生支持并发,可以同时连接多个客户端

#include "socket_udp_server.h"#include "socket_tcp_server.h"#include "socket_wrap.h"#include "ctype.h"
static char ReadBuff[BUFF_SIZE];
/** * @brief udp服务器任务 * @param None * @retval None */void vUdpServerTask(void){
int sfd, n, i; struct sockaddr_in server_addr, client_addr; socklen_t client_addr_len; int optval = 1; //创建socket udp通信 sfd = Socket(AF_INET, SOCK_DGRAM, 0); setsockopt(sfd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //绑定socket Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); client_addr_len = sizeof(client_addr); while(1){ //等待客户端发送数据 n = Recvfrom(sfd, ReadBuff, BUFF_SIZE, 0, (struct sockaddr *)&client_addr, &client_addr_len); ReadBuff[n] = '\0'; printf("recv data:%s\r\n",ReadBuff); //进行大小写转换 for(i = 0; i < n; i++){ ReadBuff[i] = toupper(ReadBuff[i]); } //写回客户端 Sendto(sfd, ReadBuff, n, 0, (struct sockaddr *)&client_addr, client_addr_len); } }



UDP客户端实现比较简单,读者可自行思考。




本系下期预告:物联网lwIP网络开发(四):并发服务器编程

如需转载,请联系我,谢谢!



【往期回顾】




硬核!SpringBoot + Vue 的开源物联网智能家居系统