lwIP TCP/IP 协议栈笔记之十八: Socket接口编程

目录

1. Socket 概述

2. LwIP 中的socket

3. Socket API

3.1 socket()

3.2 bind()

3.3 connect()

3.4 listen()

3.5 accept()

3.6 read()、recv()、recvfrom()

3.7 sendto()

3.8 send()

3.9 write()

3.10 close()

3.11 ioctl()、ioctlsocket()

3.12 setsockopt()

3.13 getsockopt()

4. 实验例程

4.1  TCP Server

4.2  TCP Client


1. Socket 概述

Socket 英文原意是“孔”或者“插座”的意思,在网络编程中,通常将其称之为“套接字”,当前网络中的主流程序设计都是使用Socket 进行编程的,因为它简单易用,更是一个标准,能在不同平台很方便移植。

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

总之,套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。

Socket最初是加利福尼亚大学Berkeley分校为Unix系统开发的网络通信接口。后来随着TCP/IP网络的发展,Socket成为最为通用的应用程序接口,也是在Internet上进行应用开发最为通用的API。

为了能让更多开发者直接上手LwIP 的编程,专门设计了LwIP 的第三种编程接口——Socket API,它兼容BSD Socket。

Socket 虽然是能在多平台移植,但是LwIP 中的Socket 并不完善,因为LwIP 设计之初就是为了在嵌入式平台中使用,它只实现了完整Socket 的部分功能,不过,在嵌入式平台中,这些功能早已足够。

2. LwIP 中的socket

在LwIP 中,Socket API 是基于NETCONN API 之上来实现的,系统最多提供MEMP_NUM_NETCONN 个netconn 连接结构,因此决定Socket 套接字的个数也是那么多个。

为了更好对netconn 进行封装,LwIP 还定义了一个套接字结构体——lwip_sock(称之为Socket 连接结构),每个lwip_sock 内部都有一个netconn 的指针,实现了对netconn 的再次封装。

LwIP 定义了一个lwip_sock 类型的sockets数组,通过套接字就可以直接索引并且访问这个结构体了,这也是为什么套接字是一个整数的原因,lwip_sock 结构体是比较简单的,因为基本上全是依赖netconn 实现。

#define NUM_SOCKETS MEMP_NUM_NETCONN    // 默认是4

/** 全局可用套接字数组 **/
static struct lwip_sock sockets[NUM_SOCKETS];

union lwip_sock_lastdata {
  struct netbuf *netbuf;
  struct pbuf *pbuf;
};

/** 包含用于套接字的所有内部指针和状态*/
struct lwip_sock {
  /** 套接字当前是在netconn 上构建的,每个套接字都有一个netconn*/
  struct netconn *conn;
  /** 从上一次读取中留下的数据 */
  union lwip_sock_lastdata lastdata;
#if LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL
  /** number of times data was received, set by event_callback(),
      tested by the receive and select functions */
  s16_t rcvevent;
  /** number of times data was ACKed (free send buffer), set by event_callback(),
      tested by select */
  u16_t sendevent;
  /** error happened for this socket, set by event_callback(), tested by select */
  u16_t errevent;
  /** 使用select 等待此套接字的线程数 */
  SELWAIT_T select_waiting;
#endif /* LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL */
#if LWIP_NETCONN_FULLDUPLEX
  /* counter of how many threads are using a struct lwip_sock (not the 'int') */
  u8_t fd_used;
  /* status of pending close/delete actions */
  u8_t fd_free_pending;
#define LWIP_SOCK_FD_FREE_TCP  1
#define LWIP_SOCK_FD_FREE_FREE 2
#endif
};

3. Socket API

3.1 socket()

向内核申请一个套接字,在本质上该函数其实就是对netconn_new()函数进行了封装,虽然说不是直接调用它,但是主体完成的工作就做了 netconn_new()函数的事情,而且该函数本质是一个宏定义.

/** @ingroup socket */
#define socket(domain,type,protocol)              lwip_socket(domain,type,protocol)

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

#define AF_INET         2

/* Socket protocol types (TCP/UDP/RAW) */
#define SOCK_STREAM     1
#define SOCK_DGRAM      2
#define SOCK_RAW        3

参数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。

当申请套接字成功的时候,该函数返回一个int 类型的值,也是Socket 描述符,用户通过这个值可以索引到一个Socket 连接结构——lwip_sock,当申请套接字失败时,该函数返回-1。

3.2 bind()

该函数的功能与netconn_bind()函数是一样的,用于服务器端绑定套接字与网卡信息,实际上就是对netconn_bind()函数进行了封装,可以将一个申请成功的套接字与网卡信息进行绑定。

/** @ingroup socket */
#define bind(s,name,namelen)                      lwip_bind(s,name,namelen)

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

参数s : 表示要绑定的Socket 套接字

参数name: 是一个指向sockaddr 结构体的指针,其中包含了网卡的IP 地址、端口号等重要的信息,LwIP 为了更好描述这些信息,使用了sockaddr 结构体来定义了必要的信息的字段,它常被用于Socket API 的很多函数中,我们在使用bind()的时候,只需要直接填写相关字段即可.

参数namelen: 指定了name 结构体的长度

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

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

/* members are in network byte order */
struct sockaddr_in {
  u8_t            sin_len;        // 长度
  sa_family_t     sin_family;     // 协议簇  uint8_t
  in_port_t       sin_port;       // 端口    uint16_t
  struct in_addr  sin_addr;       // 地址    uint32_t
#define SIN_ZERO_LEN 8
  char            sin_zero[SIN_ZERO_LEN]; 
};

这个结构体的前两个字段是与sockaddr 结构体的前两个字段一致,而剩下的字段就是sa_data 连续的14 字节信息里面的内容,只不过从新定义了成员变量而已,sin_port 字段是我们需要填写的端口号信息,sin_addr 字段是我们需要填写的IP 地址信息,剩下sin_zero区域的8 字节保留未用.

使用例程:

#define PORT              5001
#define IP_ADDR        "192.168.0.181"


int sock = -1;  
struct sockaddr_in server_addr;

sock = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;      
server_addr.sin_port = htons(PORT);   
server_addr.sin_addr.s_addr = inet_addr(IP_ADDR);
memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));    

if (bind(sock, (struct sockaddr *)&server_addr, 
                sizeof(struct sockaddr)) == -1) {
    ;
}

3.3 connect()

函数的作用与netconn_connect()函数的作用基本一致,因为就是封装了netconn_connect()函数。它用于客户端中,将Socket 与远端IP 地址、端口号进行绑定,在TCP 客户端连接中,调用这个函数将发生握手过程(会发送一个TCP 连接请求),并最终建立新的TCP 连接,而对于UDP 协议来说,调用这个函数只是在UDP 控制块中记录远端IP 地址与端口号,而不发送任何数据,参数信息与bind()函数是一样的.

/** @ingroup socket */
#define connect(s,name,namelen)                   lwip_connect(s,name,namelen)

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

3.4 listen()

函数是对netconn_listen()函数的封装,只能在TCP 服务器中使用,让服务器进入监听状态,等待远端的连接请求,LwIP 中可以接收多个客户端的连接,因此参数backlog 指定了请求队列的大小.

/** @ingroup socket */
#define listen(s,backlog)                         lwip_listen(s,backlog)

int
lwip_listen(int s, int backlog);

3.5 accept()

accept()函数与netconn_accept()函数作用一样,用于TCP 服务器中,等待着远端主机的连接请求,并且建立一个新的TCP 连接,在调用这个函数之前需要通过调用listen()函数让服务器进入监听状态。accept()函数的调用会阻塞应用线程直至与远程主机建立TCP 连接。参数addr 是一个返回结果参数,它的值由accept()函数设置,其实就是远程主机的地址与端口号等信息,当新的连接已经建立后,远端主机的信息将保存在连接句柄中,它能够唯一的标识某个连接对象。同时函数返回一个int 类型的套接字描述符,根据它能索引到连接结构,如果连接失败则返回-1.

/** @ingroup socket */
#define accept(s,addr,addrlen)                    lwip_accept(s,addr,addrlen)

int
lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen);

3.6 read()、recv()、recvfrom()

read()与recv()函数的核心是调用recvfrom()函数,而recvfrom()函数是基于netconn_recv()函数来实现的,recv()与read()函数用于从Socket 中接收数据,它们可以是TCP 协议和UDP 协议

/** @ingroup socket */
#define read(s,mem,len)                           lwip_read(s,mem,len)

ssize_t
lwip_read(int s, void *mem, size_t len)
{
  return lwip_recvfrom(s, mem, len, 0, NULL, NULL);
}

/** @ingroup socket */
#define recv(s,mem,len,flags)                     lwip_recv(s,mem,len,flags)

ssize_t
lwip_recv(int s, void *mem, size_t len, int flags)
{
  return lwip_recvfrom(s, mem, len, flags, NULL, NULL);
}

ssize_t
lwip_recvfrom(int s, void *mem, size_t len, int flags,
              struct sockaddr *from, socklen_t *fromlen);

men 参数记录了接收数据的缓存起始地址,

len 用于指定接收数据的最大长度,如果函数能正确接收到数据,将会返回一个接收到数据的长度,否则将返回-1,若返回值为0,表示连接已经终止,应用程序可以根据返回的值进行不一样的操作。

recv()函数包含一个flags 参数,我们暂时可以直接忽略它,设置为0 即可。注意,如果接收的数据大于用户提供的缓存区,那么多余的数据会被直接丢弃.

3.7 sendto()

函数主要是用于UDP 协议传输数据中,它向另一端的UDP 主机发送一个UDP 报文,本质上是对netconn_send()函数的封装,参数data 指定了要发送数据的起始地址,而size 则指定数据的长度,参数flag 指定了发送时候的一些处理,比如外带数据等,此时我们不需要理会它,一般设置为0 即可,参数to 是一个指向sockaddr 结构体的指针,在这里需要我们自己提供远端主机的IP 地址与端口号,并且用tolen 参数指定这些信息的长度

/** @ingroup socket */
#define sendto(s,dataptr,size,flags,to,tolen)     lwip_sendto(s,dataptr,size,flags,to,tolen)


ssize_t
lwip_sendto(int s, const void *data, size_t size, int flags,
            const struct sockaddr *to, socklen_t tolen);

3.8 send()

send()函数可以用于UDP 协议和TCP 连接发送数据。在调用send()函数之前,必须使用connect()函数将远端主机的IP 地址、端口号与Socket 连接结构进行绑定。对于UDP 协议,send()函数将调用lwip_sendto()函数发送数据,而对于TCP 协议,将调用netconn_write_partly()函数发送数据。相对于sendto()函数,参数基本是没啥区别的,但无需我们设置远端主机的信息,更加方便操作,因此这个函数在实际中使用也是很多的

/** @ingroup socket */
#define send(s,dataptr,size,flags)                lwip_send(s,dataptr,size,flags)

ssize_t
lwip_send(int s, const void *data, size_t size, int flags);

3.9 write()

这个函数一般用于处于稳定的TCP 连接中传输数据,当然也能用于UDP 协议中,它也是基于lwip_send 上实现的,但是无需我们设置flag 参数

/** @ingroup socket */
#define write(s,dataptr,len)                      lwip_write(s,dataptr,len)


ssize_t
lwip_write(int s, const void *data, size_t size)
{
  return lwip_send(s, data, size, 0);
}

3.10 close()

close()函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符索引到连接结构,该函数的本质是对netconn_delete()函数的封装(真正处理的函数是netconn_prepare_delete()),如果连接是TCP 协议,将产生一个请求终止连接的报文发送到对端主机中,如果是UDP 协议,将直接释放UDP 控制块的内容

/** @ingroup socket */
#define close(s)                                  lwip_close(s)

int
lwip_close(int s);

3.11 ioctl()、ioctlsocket()

两个函数,其实是一样的,本质是宏定义,都是调用lwip_ioctl()函数,它用于获取与设置套接字相关的操作参数.

s:一个标识套接口的描述字。

cmd:对套接口s的操作命令。

argp:指向cmd命令所带参数的指针

参数cmd 指明对套接字的操作命令,在LwIP中只支持FIONREAD 与FIONBIO 命令:

  • FIONREAD 命令确定套接字s 自动读入的数据量,这些数据已经被接收,但应用线程并未读取的,所以可以使用这个函数来获取这些数据的长度,在这个命令状态下,argp 参数指向一个无符号长整型,用于保存函数的返回值(即未读数据的长度)。如果套接字是SOCK_STREAM类型,则FIONREAD 命令会返回recv()函数中所接收的所有数据量,这通常与在套接字接收缓存队列中排队的数据总量相同;而如果套接字是SOCK_DGRAM类型的,则FIONREAD 命令将返回在套接字接收缓存队列中排队的第一个数据包大小。
  • FIONBIO 命令用于允许或禁止套接字的非阻塞模式。在这个命令下,argp 参数指向一个无符号长整型,如果该值为0 则表示禁止非阻塞模式,而如果该值非0 则表示允许非阻塞模式则。当创建一个套接字的时候,它就处于阻塞模式,也就是
    说非阻塞模式被禁止,这种情况下所有的发送、接收函数都会是阻塞的,直至发送、接收成功才得以继续运行;而如果是非阻塞模式下,所有的发送、接收函数都是不阻塞的,如果发送不出去或者接收不到数据,将直接返回错误代码给用户,这就需要用户对这些“意外”情况进行处理,保证代码的健壮性,这与BSD Socket 是一致的。
/** @ingroup socket */
#define ioctlsocket(s,cmd,argp)                   lwip_ioctl(s,cmd,argp)

/** @ingroup socket */
#define ioctl(s,cmd,argp)                         lwip_ioctl(s,cmd,argp)


int
lwip_ioctl(int s, long cmd, void *argp);

3.12 setsockopt()

/** @ingroup socket */
#define setsockopt(s,level,optname,opval,optlen) lwip_setsockopt(s,level,optname,opval,optlen)

int
lwip_setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);

这个函数是用于设置套接字的一些选项的,参数level 有多个常见的选项,如:

SOL_SOCKET:表示在Socket 层。

IPPROTO_TCP:表示在TCP 层。

IPPROTO_IP: 表示在IP 层。

参数optname 表示该层的具体选项名称,比如:

  • 1. 对于SOL_SOCKET 选项,可以是SO_REUSEADDR(允许重用本地地址和端口)、SO_SNDTIMEO(设置发送数据超时时间)、SO_SNDTIMEO(设置接收数据超时时间)、SO_RCVBUF(设置发送数据缓冲区大小)等等。
  • 2. 对于IPPROTO_TCP 选项,可以是TCP_NODELAY(不使用Nagle 算法)、TCP_KEEPALIVE(设置TCP 保活时间)等等。
  • 3. 对于IPPROTO_IP 选项,可以是IP_TTL(设置生存时间)、IP_TOS(设置服务类型)等等。

3.13 getsockopt()

这个函数与setsockopt()函数的选项参数及名称都是差不多的,只不过是作用是获得这些选项信息在这里就不过多讲解

4. 实验例程

lwIP TCP/IP 协议栈笔记之十八: Socket接口编程_第1张图片

4.1  TCP Server

lwIP TCP/IP 协议栈笔记之十八: Socket接口编程_第2张图片

#define PORT              5001
#define RECV_DATA         (1024)


static void 
tcpecho_thread(void *arg)
{
  int sock = -1,connected;
  char *recv_data;
  struct sockaddr_in server_addr,client_addr;
  socklen_t sin_size;
  int recv_data_len;
  
  recv_data = (char *)pvPortMalloc(RECV_DATA);
  if (recv_data == NULL)
  {
      printf("No memory\n");
      goto __exit;
  }
  
  sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0)
  {
      printf("Socket error\n");
      goto __exit;
  }
  
  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = INADDR_ANY;
  server_addr.sin_port = htons(PORT);

  memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
  
  if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
  {
      printf("Unable to bind\n");
      goto __exit;
  }
  
  if (listen(sock, 5) == -1)
  {
      printf("Listen error\n");
      goto __exit;
  }
  
  while(1)
  {
    sin_size = sizeof(struct sockaddr_in);

    connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);

    printf("new client connected from (%s, %d)\n",
            inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    {
      int flag = 1;
      
      setsockopt(connected,
                 IPPROTO_TCP,     /* set option at TCP level */
                 TCP_NODELAY,     /* name of option */
                 (void *) &flag,  /* the cast is historical cruft */
                 sizeof(int));    /* length of option value */
    }
    
    while(1)
    {
      recv_data_len = recv(connected, recv_data, RECV_DATA, 0);
      
      if (recv_data_len <= 0) 
        break;
      
      printf("recv %d len data\n",recv_data_len);
      
      write(connected,recv_data,recv_data_len);
      
    }
    if (connected >= 0) 
      closesocket(connected);
    
    connected = -1;
  }
__exit:
  if (sock >= 0) closesocket(sock);
  if (recv_data) free(recv_data);
}

4.2  TCP Client

lwIP TCP/IP 协议栈笔记之十八: Socket接口编程_第3张图片

#define PORT              5001
#define IP_ADDR        "192.168.0.100"

static void client(void *thread_param)
{
  int sock = -1;
  struct sockaddr_in client_addr;
  
  uint8_t send_buf[]= "This is a TCP Client test...\n";
  
  while(1)
  {
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
      printf("Socket error\n");
      vTaskDelay(10);
      continue;
    } 

    client_addr.sin_family = AF_INET;      
    client_addr.sin_port = htons(PORT);   
    client_addr.sin_addr.s_addr = inet_addr(IP_ADDR);
    memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));    

    if (connect(sock, 
               (struct sockaddr *)&client_addr, 
                sizeof(struct sockaddr)) == -1) 
    {
        printf("Connect failed!\n");
        closesocket(sock);
        vTaskDelay(10);
        continue;
    }                                           
    
    printf("Connect to iperf server successful!\n");
    
    while (1)
    {
      if(write(sock,send_buf,sizeof(send_buf)) < 0)
        break;
   
      vTaskDelay(1000);
    }
    
    closesocket(sock);
  }

}

 注意: PC与单板连接的时候,网络调试助手,必须保证防火墙是允许通信的,否则可能被拦截而造成失败。

你可能感兴趣的:(TCP/IP,嵌入式开发,LWIP)