Socket接口编程

本文介绍了在STM32嵌入式系统中使用LwIP库进行网络编程的Socket接口,包括IP地址转换、套接字结构体、API函数如socket、connect、write、read等的详细使用方法,适用于TCP和UDP通信。通过Cubemx配置网络、PHY、LwIP和FreeRTOS,并提供了UDP、TCP客户端和服务端实验的实践指导。
摘要由CSDN通过智能技术生成

简介

1、Socket 英文原意是“孔”或者“插座”的意思,在网络编程中,通常将其称之为“套接字”,当前网络中的主流程序设计都是使用 Socket 进行编程的,因为它简单易用,更是一个标准,能在不同平台很方便移植。
2、socket是统一的编程接口,具有高移植性(但是没有Windows或者linux上这么完善,但是一般的应用都是够了的),而netconn是lwip所独有的。
3、在 Socket 中,它使用一个套接字来记录网络的一个连接,套接字是一个整数,就像我们操作文件一样,利用一个文件描述符,可以对它打开、读、写、关闭等操作,类似的,在网络中,我们也可以对 Socket 套接字进行这样子的操作,比如开启一个网络的连接、读取连接主机发送来的数据、向连接的主机发送数据、终止连接等操作。
4、在 LwIP 中,Socket API 是 基 于 NETCONN API 之 上 来 实 现 的, 系 统 最 多 提 供MEMP_NUM_NETCONN 个 netconn 连接结构,因此 Socket 套接字的个数也是那么多个。
5、为了更好对 netconn 进行封装,LwIP 还定义了一个套接字结构体——lwip_sock(我称之为 Socket连接结构),每个 lwip_sock 内部都有一个 netconn 的指针,实现了对 netconn 的再次封装
6、LwIP 定义了一个 lwip_sock 类型的 sockets 数组,通过套接字就可以直接索引并且访问这个结构体了,这也是为什么套接字是一个整数的原因,lwip_sock 结构体是比较简单的,因为基本上全是依赖 netconn 实现。
7、socket是一套用于不同主机间通信的API。网络套接字是IP地址与端口的组合,IP地址用来唯一标识网络设备,而端口号用于区分具体将数据发送到哪个应用上。通过socket可以建立一条不同主机不同应用之间的虚拟数据通道,并且其是点对点的
参考文章

IP地址转换

client_sock = accept(sock, (struct sockaddr *)&client_addr, &clientaddr_size);//会一直阻塞在这里,直至与远程主机建立 TCP 连接
		
printf("new client connected from (%s, %d)\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

char *inet_ntoa(struct in_addr in);

  • 将网络传输的二进制数值转化为成点分十进制的ip地址(也就是变成一个字符串格式)。
  • inet_ntoa 函数转换网络字节排序的地址为标准的ASCII以点分开的地址,该函数返回指向点分开的字符串地址(如192.168.1.10)的指针,该字符串的空间为静态分配的,这意味着在第二次调用该函数时,上一次调用将会被重写(复盖),所以如果需要保存该串最后复制出来自己管理!

inet_ntoa(变成字符串)和inet_addr(字符串变成传输的值)这两个只能转换ipv4
讲的非常好的一篇文章

inet_addr: 将字符串形式的IP地址转换为按网络字节顺序的整型值

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

int main()
{
   
    struct in_addr ipAddr;
    // 将字符串形式的IP地址转换为按网络字节顺序的整型值
    ipAddr.S_un.S_addr = inet_addr("192.168.19.36"); 
    unsigned int dip;
    printf("%u", ipAddr.S_un.S_addr);
    return 0;
}

结构体

套接字结构体——lwip_sock

  • 为了更好对 netconn 进行封装,LwIP 还定义了一个套接字结构体——lwip_sock(我称之为 Socket连接结构)
  • 每个 lwip_sock 内部都有一个 netconn 的指针,实现了对 netconn 的再次封装
  • 那怎么找到 lwip_sock 这个结构体呢?LwIP 定义了一个 lwip_sock 类型的 sockets 数组,通过套接字就可以直接索引并且访问这个结构体了,这也是为什么套接字是一个整数的原因
  • lwip_sock 结构体是比较简单的,因为基本上全是依赖 netconn 实现
/** 包含用于套接字的所有内部指针和状态 */
struct lwip_sock
{
   
/** 套接字当前是在 netconn 上构建的,每个套接字都有一个 netconn */
struct netconn *conn;
/** 从上一次读取中留下的数据 */
union lwip_sock_lastdata lastdata;
/** 收到数据的次数由 event_callback() 记录,下面的字段在 select 机制上使用 */
s16_t rcvevent;
/** 发送数据的次数,也是由回调函数记录的 */
u16_t sendevent;
/** Socket 上的发生的错误次数 */
u16_t errevent;
/** 使用 select 等待此套接字的线程数 */
SELWAIT_T select_waiting;
};
#define NUM_SOCKETS MEMP_NUM_NETCONN
/** 全局可用套接字数组(默认是 4) */
static struct lwip_sock sockets[NUM_SOCKETS];
union lwip_sock_lastdata
{
   
struct netbuf *netbuf;
struct pbuf *pbuf;
};

sockaddr 结构体

咋一看这个结构体,好像没啥信息要我们填写的,确实也是这样子,我们需要填写的 IP 地址与端口号等信息,都在 sa_data 连续的 14 字节信息里面,但是这个数据对我们不友好,因此 LwIP还定义了另一个对开发者更加友好的结构体——sockaddr_in,我们一般也是用这个结构体

struct sockaddr
{
   
	u8_t sa_len; /* 长度 */
	sa_family_t sa_family; /* 协议簇 */
	char sa_data[14]; /* 连续的 14 字节信息 */
};

sockaddr_in 结构体

  • 这个结构体的前两个字段是与 sockaddr 结构体的前两个字段一致,而剩下的字段就是 sa_data 连续的 14 字节信息里面的内容,只不过从新定义了成员变量而已
  • sin_port 字段是我们需要填写的端口号信息
  • sin_addr 字段是我们需要填写的 IP 地址信息 INADDR_ANY就是所有IP地址的意思
  • sin_zero 区域的 8 字节保留未用。
#if LWIP_IPV4
/* members are in network byte order */
struct sockaddr_in {
   
 	  u8_t            sin_len;
	  sa_family_t     sin_family;
	  in_port_t       sin_port;
	  struct in_addr  sin_addr;
	 #define SIN_ZERO_LEN 8
 	  char            sin_zero[SIN_ZERO_LEN];
};
#endif /* LWIP_IPV4 */
typedef u32_t in_addr_t;

struct in_addr {
   
  in_addr_t s_addr;
};

ERR_OK = 0

/** Definitions for error constants. */
typedef enum {
   
/** No error, everything OK. */
  ERR_OK         = 0,
/** Out of memory error.     */
  ERR_MEM        = -1,
/** Buffer error.            */
  ERR_BUF        = -2,
/** Timeout.                 */
  ERR_TIMEOUT    = -3,
/** Routing problem.         */
  ERR_RTE        = -4,
/** Operation in progress    */
  ERR_INPROGRESS = -5,
/** Illegal value.           */
  ERR_VAL        = -6,
/** Operation would block.   */
  ERR_WOULDBLOCK = -7,
/** Address in use.          */
  ERR_USE        = -8,
/** Already connecting.      */
  ERR_ALREADY    = -9,
/** Conn already established.*/
  ERR_ISCONN     = -10,
/** Not connected.           */
  ERR_CONN       = -11,
/** Low-level netif error    */
  ERR_IF         = -12,

/** Connection aborted.      */
  ERR_ABRT       = -13,
/** Connection reset.        */
  ERR_RST        = -14,
/** Connection closed.       */
  ERR_CLSD       = -15,
/** Illegal argument.        */
  ERR_ARG        = -16
} err_enum_t;

API

socket(domain,type,protocol)

1、简介

  • 作用: 向内核申请一个套接字,因此其返回值就是一个int类型的值,也就是Socket描述符,用户通过这个值可以索引到一个Socket 连接结构——lwip_sock。
  • 本质上该函数其实就是对 netconn_new() 函数进行了封装,虽然说不是直接调用它,但是主体完成的工作就做了 netconn_new() 函数的事情。
  • 参数domain :表示该套接字使用的协议簇,对于 TCP/IP 协议来说,该值始终为 AF_INET
  • 参数 type 指定了套接字使用的服务类型,可能的类型有 3 种:
    (1)SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接的 Socket 服务,多用于资料(如文件)传输,如 TCP 协议
    (2)SOCK_DGRAM:是提供无保障的面向消息的 Socket 服务,主要用于在网络上发广播信息,如 UDP 协议,提供无连接不可靠的数据报交付服务。
    (3)SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。
  • 参数 protocol 指定了套接字使用的协议,在 IPv4 中,只有 TCP 协议提供 SOCK_STREAM 这种可靠的服务,只有 UDP 协议提供 SOCK_DGRAM 服务,对于这两种协议,protocol 的值均为 0

2、使用

//TCP客户端
int sock = -1;
sock=socket(AF_INET,SOCK_STREAM,0);
		if(sock < 0)
		{
   
			printf("Socket error\n");
			osDelay(1);
			continue;  //这个continue是不执行下面的代码,从这个循环的头开始重新走一遍
		}

3、源码

#define socket(domain,type,protocol)              lwip_socket(domain,type,protocol)
int
lwip_socket(int domain, int type, int protocol)
{
   
  struct netconn *conn;
  int i;

  LWIP_UNUSED_ARG(domain); /* @todo: check this */

  /* create a netconn */
  switch (type) {
   
    case SOCK_RAW:
      conn = netconn_new_with_proto_and_callback(DOMAIN_TO_NETCONN_TYPE(domain, NETCONN_RAW),
             (u8_t)protocol, DEFAULT_SOCKET_EVENTCB);
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_socket(%s, SOCK_RAW, %d) = ",
                                  domain == PF_INET ? "PF_INET" : "UNKNOWN", protocol));
      break;
    case SOCK_DGRAM:
      conn = netconn_new_with_callback(DOMAIN_TO_NETCONN_TYPE(domain,
                                       ((protocol == IPPROTO_UDPLITE) ? NETCONN_UDPLITE : NETCONN_UDP)),
                                       DEFAULT_SOCKET_EVENTCB);
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_socket(%s, SOCK_DGRAM, %d) = ",
                                  domain == PF_INET ? "PF_INET" : "UNKNOWN", protocol));
#if LWIP_NETBUF_RECVINFO
      if (conn) {
   
        /* netconn layer enables pktinfo by default, sockets default to off */
        conn->flags &= ~NETCONN_FLAG_PKTINFO;
      }
#endif /* LWIP_NETBUF_RECVINFO */
      break;
    case SOCK_STREAM:
      conn = netconn_new_with_callback(DOMAIN_TO_NETCONN_TYPE(domain, NETCONN_TCP), DEFAULT_SOCKET_EVENTCB);
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_socket(%s, SOCK_STREAM, %d) = ",
                                  domain == PF_INET ? "PF_INET" : "UNKNOWN", protocol));
      break;
    default:
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_socket(%d, %d/UNKNOWN, %d) = -1\n",
                                  domain, type, protocol));
      set_errno(EINVAL);
      return -1;
  }

  if (!conn) {
   
    LWIP_DEBUGF(SOCKETS_DEBUG, ("-1 / ENOBUFS (could not create netconn)\n"));
    set_errno(ENOBUFS);
    return -1;
  }

  i = alloc_socket(conn, 0);

  if (i == -1) {
   
    netconn_delete(conn);
    set_errno(ENFILE);
    return -1;
  }
  conn->socket = i;
  done_socket(&sockets[i - LWIP_SOCKET_OFFSET]);
  LWIP_DEBUGF(SOCKETS_DEBUG, ("%d\n", i));
  set_errno(0);
  return i;
}

connect(s,name,namelen)

1、简介

  • 作用与 netconn_connect() 函数的作用基本一致,用于客户端中,就是建立连接,因为就是封装了 netconn_connect() 函数。
  • TCP 客户端连接中,调用这个函数将发生握手过程(会发送一个 TCP 连接请求),并最终建立新的 TCP 连接。
  • 而对于 UDP协议来说,调用这个函数只是在 UDP 控制块中记录远端 IP 地址与端口号,而不发送任何数据,参数信息与 bind() 函数是一样的。
  • 参数 s 是表示要绑定的 Socket 套接字,注意了,这个套机字必须是从 socket() 函数中返回的索引,否则将无法完成绑定操作。
  • 参数 name 是一个指向 sockaddr 结构体的指针,其中包含了网卡的 IP 地址、端口号等重要的信息,LwIP 为了更好描述这些信息,使用了 sockaddr 结构体来定义了必要的信息的字段,它常被用于Socket API 的很多函数中。
  • 参数 namelen 指定了 name 结构体的长度。
  • 返回值: 当调用成功时,函数返回0;否则返回-1

2、使用

    struct sockaddr_in client_addr;
	//这一段用来设置IP地址
	uint8_t Remote_IP_Addr[4]; 	//远端ip地址
	//定义目标IP地址
	//定义一个u8的数组,每一项存一个。最后使用IP4_ADDR函数将其整合在一起
	Remote_IP_Addr[0]=192;
	Remote_IP_Addr[1]=168;
	Remote_IP_Addr[2]=0;
	Remote_IP_Addr[3]=4;//8000
	IP4_ADDR(&server_ipaddr,Remote_IP_Addr[0],Remote_IP_Addr[1],Remote_IP_Addr[2],Remote_IP_Addr[3]);

   	 	 client_addr.sin_family = AF_INET;            //固定就是这个
   		 client_addr.sin_port = htons(8000);     //远端端口号
   		 client_addr.sin_addr.s_addr = server_ipaddr.addr;   //IP地址 u32_t 
   		 memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));   //8字节保留未用 
		
		err = connect(sock,(struct sockaddr *)&client_addr,sizeof(client_addr));
		
		if(err != ERR_OK)  
		{
   
        	closesocket(sock);
        	osDelay(10);
       	    continue;//重新开启一遍这个while
        }                 	
	  printf("Connect to server successful!\r\n");

3、源码

#define connect(s,name,namelen)                   lwip_connect(s,name,namelen)
int
lwip_connect(int s, const struct sockaddr *name, socklen_t namelen)
{
   
  struct lwip_sock *sock;
  err_t err;

  sock = get_socket(s);
  if (!sock) {
   
    return -1;
  }

  if (!SOCK_ADDR_TYPE_MATCH_OR_UNSPEC(name, sock)) {
   
    /* sockaddr does not match socket type (IPv4/IPv6) */
    sock_set_errno(sock, err_to_errno(ERR_VAL));
    done_socket(sock);
    return -1;
  }

  LWIP_UNUSED_ARG(namelen);
  if (name->sa_family == AF_UNSPEC) {
   
    LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_connect(%d, AF_UNSPEC)\n", s));
    err = netconn_disconnect(sock->conn);
  } else {
   
    ip_addr_t remote_addr;
    u16_t remote_port;

    /* check size, family and alignment of 'name' */
    LWIP_ERROR("lwip_connect: invalid address", IS_SOCK_ADDR_LEN_VALID(namelen) &&
               IS_SOCK_ADDR_TYPE_VALID_OR_UNSPEC(name) && IS_SOCK_ADDR_ALIGNED(name),
               sock_set_errno(sock, err_to_errno(ERR_ARG)); done_socket(sock); return -1;);

    SOCKADDR_TO_IPADDR_PORT(name, &remote_addr
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

成草

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值