TCP/IP协议栈之LwIP(十)---Socket API编程

本文详细介绍了LwIP中的Socket API编程,包括BSD Socket的起源和重要性,以及Socket API的实现,如socket、bind、connect、listen、accept等关键函数。此外,还探讨了基于Select的并发服务器设计,阐述了Select函数的工作原理和在并发服务器中的应用。通过示例代码展示了如何使用Socket API创建并发服务器,强调了在多线程环境下处理套接字事件的注意事项。
摘要由CSDN通过智能技术生成

一、BSD Socket简介

BSD Socket最初是由加州伯克利大学为Unix系统开发出来的,因此也被称为伯克利套接字(Internet Berkeley Sockets),它是一种采用C语言进程间通信库的应用程序接口(API),经常用在计算机网络间的通信,大多数其他的编程语言也都使用类似的接口。

BSD Socket作为一种API,允许不同主机或者同一个计算机上的不同进程之间的通信,它支持多种I/O设备和驱动,但是具体的实现是依赖操作系统的。这种接口对于TCP/IP是必不可少的,所以是互联网的基础技术之一,所有现代的操作系统都实现了BSD Socket API,因为它已经是连接互联网的标准接口了,也是当前主机网络程序设计领域的事实标准。

为了能更大程度上方便开发者将其他平台上的网络应用程序移植到LwIP上,也为了能让更多开发者快速上手LwIP,LwIP内核作者为LwIP设计了第三种应用程序编程接口,即Socket API,兼容于BSD Socket,但是受嵌入式处理器资源和性能的限制,部分Socket接口并未在LwIP中完全实现,如果想了解完整的BSD Socket可以参考Berkeley套接字BSD套接字API两篇文章。同时,Socket API是基于Sequential API来实现的,所以应用程序的执行效率较基于后者实现的程序效率更低,抽象程度越高,执行效率的损失越大,读者需要平衡取舍执行效率与通用可移植性。

BSD中用一个套接字记录网络中的一个连接,套接字就像一个普通的文件,应用程序可以像操作普通文件那样操作它,例如打开、关闭、读写等。文件描述符是一个整数,所以套接字描述符也是一个整数,通过它可以索引到内核中描述连接的具体结构。

二、Socket API实现

2.1 Socket结构描述

在LwIP抽象出来的Socket API中,内核为用户提供了最多NUM_SOCKETS个可使用的socket描述符,并定义了结构体lwip_socket(对netconn结构的封装和增强)来描述一个具体连接。内核定义了数组sockets,通过一个Socket描述符就可以索引得到相应的连接结构lwip_socket,从而实现对连接的操作。连接结构lwip_socket的数据结构实现如下:

// rt-thread\components\net\lwip-1.4.1\src\api\sockets.c

#define NUM_SOCKETS MEMP_NUM_NETCONN

/** Contains all internal pointers and states used for a socket */
struct lwip_sock {
   
  /** sockets currently are built on netconns, each socket has one netconn */
  struct netconn *conn;
  /** data that was left from the previous read */
  void *lastdata;
  /** offset in the data that was left from the previous read */
  u16_t lastoffset;
  /** 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; 
  /** last error that occurred on this socket */
  int err;
  /** counter of how many threads are waiting for this socket using select */
  int select_waiting;
};

/** The global array of available sockets */
static struct lwip_sock sockets[NUM_SOCKETS];

lwip_socket结构是对连接结构netconn的再次封装,在内核内部,对lwip_socket的操作最终都会映射到对netconn结构的操作上。

描述socket的结构体lwip_sock相对比较简单,它基于内核netconn来实现所有逻辑,conn指向了与socket对应的netconn结构;另外在socket数据接收时,其实它是利用netconn相关的接收函数获得一个pbuf(对于TCP)或者一个netbuf(对于UDP)数据,而这二者封装的数据可能大于socket用户指定的数据接收长度,因此在这种情况下,这两个数据包需要暂时保存在socket中,以待用户下一次读取,这里lastdata就用于指向未被用户完全读取的数据包,而lastoffset则指向了未读取的数据在数据包中的偏移。

lwip_sock最后的五个字段是为select机制实现时使用,select函数可通过事件机制监听一个或多个套接字状态的变化,常用于并发服务器编程,后面再专门介绍其实现过程。

2.2 Socket API接口函数

  • socket
    函数原型:int socket(int domain, int type, int protocol)

该函数功能是向内核申请一个套接字,本质上,socket是对Sequential API函数netconn_new的封装,三个参数分别如下:

参数名称 参数取值
domain
为创建的套接字指定使用的协议簇
AF_INET 表示IPv4网络协议
AF_INET6 表示IPv6
AF_UNIX 表示本地套接字(使用一个文件)
type
指定协议簇中的具体服务类型
SOCK_STREAM (可靠数据流交付服务,比如TCP)
SOCK_DGRAM (无连接数据报交付服务,比如UDP)
SOCK_RAW (原始套接字,比如RAW)
protocol
指定实际使用的具体协议
常见的有IPPROTO_TCP、IPPROTO_UDP等
若设置为"0",表示根据前两个参数使用缺省协议

该函数的返回值为一个有效的socket描述符,内核使用这个函数就可以索引到描述连接的具体结构lwip_socket。当申请失败时,该函数返回-1。该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\sockets.h
#define socket(a,b,c)         lwip_socket(a,b,c)

// rt-thread\components\net\lwip-1.4.1\src\api\sockets.c
int lwip_socket(int domain, int type, int protocol)
{
   
  struct netconn *conn;
  int i;

  /* create a netconn */
  switch (type) {
   
  case SOCK_RAW:
    conn = netconn_new_with_proto_and_callback(NETCONN_RAW, (u8_t)protocol, event_callback);
    break;
  case SOCK_DGRAM:
    conn = netconn_new_with_callback( (protocol == IPPROTO_UDPLITE) ?
                 NETCONN_UDPLITE : NETCONN_UDP, event_callback);
    break;
  case SOCK_STREAM:
    conn = netconn_new_with_callback(NETCONN_TCP, event_callback);
    if (conn != NULL) {
   
      /* Prevent automatic window updates, we do this on our own! */
      netconn_set_noautorecved(conn, 1);
    }
    break;
  default:
    set_errno(EINVAL);
    return -1;
  }
  if (!conn) {
   
    set_errno(ENOBUFS);
    return -1;
  }
  i = alloc_socket(conn, 0);

  if (i == -1) {
   
    netconn_delete(conn);
    set_errno(ENFILE);
    return -1;
  }
  conn->socket = i;
  set_errno(0);
  return i;
}

static int alloc_socket(struct netconn *newconn, int accepted)
{
   
  int i;
  SYS_ARCH_DECL_PROTECT(lev);

  /* allocate a new socket identifier */
  for (i = 0; i < NUM_SOCKETS; ++i) {
   
    /* Protect socket array */
    SYS_ARCH_PROTECT(lev);
    if (!sockets[i].conn) {
   
      sockets[i].conn       = newconn;
      /* The socket is not yet known to anyone, so no need to protect
         after having marked it as used. */
      SYS_ARCH_UNPROTECT(lev);
      sockets[i].lastdata   = NULL;
      sockets[i].lastoffset = 0;
      sockets[i].rcvevent   = 0;
      /* TCP sendbuf is empty, but the socket is not yet writable until connected
       * (unless it has been created by accept()). */
      sockets[i].sendevent  = (newconn->type == NETCONN_TCP ? (accepted != 0) : 1);
      sockets[i].errevent   = 0;
      sockets[i].err        = 0;
      sockets[i].select_waiting = 0;
      return i;
    }
    SYS_ARCH_UNPROTECT(lev);
  }
  return -1;
}
  • bind
    函数原型:int bind(int s, const struct sockaddr *name, socklen_t namelen)

该函数功能是将一个套接字与本地地址信息进行绑定,本质上,bind是对Sequential API函数netconn_bind的封装。作为服务器程序,通常需要调用该函数将套接字绑定到本地的知名端口号上,这样才能响应客户端的连接请求。参数s记录了将要进行绑定的套接字对象;参数name指向一个sockaddr结构体,包含了本地IP地址和端口号等信息;参数namelen指出了结构体的长度。结构体sockaddr的定义如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\sockets.h

/* members are in network byte order */
struct sockaddr {
   
  u8_t sa_len;
  u8_t sa_family;
  char sa_data[14];
};

struct sockaddr_in {
   
  u8_t sin_len;
  u8_t sin_family;
  u16_t sin_port;
  struct in_addr sin_addr;
  char sin_zero[8];
};

// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\inet.h
/** For compatibility with BSD code */
struct in_addr {
   
  u32_t s_addr;
};

结构体sockaddr中的sa_family指向该套接字所使用的协议簇,sa_data指向了bind需要的一些本地地址信息,前2个字节用于记录端口号port,接下来4个字节用于记录IP地址,剩余的8个字节用于传递其他信息,这里暂未用到。由于sa_data以连续空间的方式存在,如果用户要填写其中的IP字段和端口port字段,会显得比较麻烦,因此socket中定义了另一种sockaddr_in结构,它与sockaddr结构对等,只是从中抽出IP地址和端口号port,方便于用于的编程操作。

该函数绑定成功时返回0,绑定失败时返回-1。该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\sockets.h
#define bind(a,b,c)           lwip_bind(a,b,c)


// rt-thread\components\net\lwip-1.4.1\src\api\sockets.c

int lwip_bind(int s, const struct sockaddr *name, socklen_t namelen)
{
   
  struct lwip_sock *sock;
  ip_addr_t local_addr;
  u16_t local_port;
  err_t err;
  const struct sockaddr_in *name_in;

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

  /* check size, familiy and alignment of 'name' */
  name_in = (const struct sockaddr_in *)(void*)name;

  inet_addr_to_ipaddr(&local_addr, &name_in->sin_addr);
  local_port = name_in->sin_port;

  err = netconn_bind(sock->conn, &local_addr, ntohs(local_port));

  if (err != ERR_OK) {
   
    sock_set_errno(sock, err_to_errno(err));
    return -1;
  }
  sock_set_errno(sock, 0);
  return 0;
}
  • connect
    函数原型:int connect(int s, const struct sockaddr *name, socklen_t namelen)

该函数功能与bind函数相对应:将套接字与目的地址信息进行绑定。该函数本质上是对Sequential API函数netconn_connect的封装,作为客户端程序,通常需要使用该函数来绑定服务器的地址信息。对于TCP连接,调用这个函数会导致客户端与服务器之间发生连接握手过程,并最终建立一条稳定的连接;对于UDP连接,该函数调用不会有任何数据包被发送,只是在连接结构中记录下服务器的地址信息。当调用成功时,函数返回0;否则返回-1。

  • getsockname / getpeername
    函数原型:int getsockname (int s, struct sockaddr *name, socklen_t *namelen);
    int getpeername (int s, struct sockaddr *name, socklen_t *namelen)

该函数功能是获取该套接字的本地地址信息 / 远端地址信息。该函数本质上是对Sequential API函数netconn_addr / netconn_peer的封装,参数name保存了从套接字获取的地址信息,参数namelen保存了从套接字获取的name结构体长度信息。当调用成功时,函数返回0;否则返回-1。

  • listen
    函数原型:int listen(int s, int backlog)

该函数只能在TCP服务器程序中使用,作用是将一个套接字置为侦听状态,以等待客户端的连接请求。该函数本质上是对Sequential API函数netconn_listen的封装,内核同时接收到多个连接请求时,需要对这些请求进行排队处理,参数backlog指明了该套接字上连接请求队列的最大长度。当调用成功时,函数返回0;否则返回-1。

  • accept
    函数原型:int accept(int s, struct sockaddr *addr, socklen_t *addrlen)

该函数也只能在TCP服务器程序中使用,作用是从套接字的连接请求队列中获取一个新建立的连接,如果请求队列为空,该函数会阻塞,直至新连接到来。该函数本质上是对Sequential API函数netconn_accept的封装,当接收到新连接后,连接另一端(客户端)的地址信息会被填入到地址结构addr中,而对应地址信息的长度被记录到addrlen中。函数返回新连接的套接字描述符,若调用失败,函数返回-1。

  • send / sendto
    函数原型:int send(int s, const void *dataptr, size_t size, int flags);
    int sendto(int s, const void *dataptr, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)

函数sendto主要在UDP连接中使用,作用是向另一端发送UDP报文。该函数本质上是对Sequential API函数netconn_send的封装,参数data和size分别指出了待发送数据的起始地址和长度;flags指明数据发送时的特殊处理,例如带外数据、紧急数据等,通常设置为0;参数to和tolen分别指明了目的地址信息及信息的长度,地址信息包含了目的IP地址和目的端口号。调用成功后,函数返回成功发送的字节数,出错则返回-1。

另一个函数send主要用于在一条已建立的连接上发送数据,因此不需要在参数中包含目的地址信息。该函数即可用于TCP程序,也可用于UDP程序,其本质是对Sequential API函数netconn_write和netconn_send的封装。调用成功后,函数返回成功发送的字节数,出错则返回-1。该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\sockets.h

#define send(a,b,c,d)         lwip_send(a,b,c,d)
#define sendto(a,b,c,d,e,f)   lwip_sendto(a,b,c,d,e,f)

/* Flags we can use with send and recv. */
#define MSG_PEEK       0x01    /* Peeks at an incoming message */
#define MSG_WAITALL    0x02    /* Unimplemented: Requests that the function block until the full amount of data requested can be returned */
#define MSG_OOB        0x04    /* Unimplemented: Requests out-of-band data. The significance and semantics of out-of-band data are protocol-specific */
#define MSG_DONTWAIT   0x08    /* Nonblocking i/o for this operation only */
#define MSG_MORE       0x10    /* Sender will send more */


// rt-thread\components\net\lwip-1.4.1\src\api\sockets.c

int lwip_send(int s, const void *data, size_t size, int flags)
{
   
  struct lwip_sock *sock;
  err_t err;
  u8_t write_flags;
  size_t written;

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

  if (sock->conn->type != NETCONN_TCP) {
   
    return lwip_sendto(s, data, size, flags, NULL, 0);
  }

  write_flags = NETCONN_COPY |
    ((flags & MSG_MORE)     ? NETCONN_MORE      : 0) |
    ((flags & MSG_DONTWAIT) ? NETCONN_DONTBLOCK : 0);
  written = 0;
  err = netconn_write_partly(sock->conn, data, size, write_flags, &written);

  sock_set_errno(sock, err_to_errno(err));
  return (err == ERR_OK ? (int)written : -1);
}

int lwip_sendto(int s, const void *data, size_t size, int flags,
       const struct sockaddr *to, socklen_t tolen)
{
   
  struct lwip_sock *sock;
  err_t err;
  u16_t short_size;
  const struct sockaddr_in *to_in;
  u16_t remote_port;
  struct netbuf buf;

  sock = get_socket(s);
  if (!sock) {
   
    return -1;
  }
  
  if (sock->conn->type == NETCONN_TCP) {
   
    return lwip_send(s, data, size, flags);
  }
  
  /* @todo: split into multiple sendto's? */
  short_size = (u16_t)size;
  to_in = (const struct sockaddr_in *)(void*)to;

  /* initialize a buffer */
  buf.p = buf.ptr = NULL;
  buf.flags = 0;
  if (to) {
   
    inet_addr_to_ipaddr(&buf.addr, &to_in->sin_addr);
    remote_port           = ntohs(to_in->sin_port);
    netbuf_fromport(&buf) = remote_port;
  } else {
   
    remote_port           = 0;
    ip_addr_set_any(&buf.addr);
    netbuf_fromport(&buf) = 0;
  }

  /* Allocate a new netbuf and copy the data into it. */
  if (netbuf_alloc(&buf, short_size) == NULL) {
   
    err = ERR_MEM;
  } else {
   
    if (sock->conn->type != NETCONN_RAW) {
   
      u16_t chksum = LWIP_CHKSUM_COPY(buf.p->payload, data, short_size);
      netbuf_set_chksum(&buf, chksum);
      err = ERR_OK;
    } else
      err = netbuf_take(&buf, data, short_size);
  }
  
  if (err == ERR_OK) {
   
    /* send the data */
    err = netconn_send(sock->conn, &buf);
  }

  /* deallocated the buffer */
  netbuf_free(&buf);
  sock_set_errno(sock, err_to_errno(err)
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

流云IoT

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

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

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

打赏作者

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

抵扣说明:

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

余额充值