Section 16 Programming with Socket Interface

What is Socket

The English original meaning of Socket is "hole" or "socket". In network programming, it is usually called "socket". The mainstream programming in the current network is programmed using Socket because it is simple and easy. It is a standard and can be easily transplanted on different platforms. This chapter explains the Socket programming interface in LwIP, because the author of LwIP specially designed the third programming interface of LwIP——Socket API, which is compatible with BSDSocket, in order to allow more developers to get started with LwIP programming directly.

Although Socket can be transplanted on multiple platforms, the Socket in LwIP is not perfect, because LwIP was originally designed for use in embedded platforms, and it only implements part of the functions of complete Sockets. However, in embedded platforms, These functions are already sufficient.

In Socket, it uses a socket to record a connection of the network. A socket is an integer, just like we operate a file. Using a file descriptor, it can be opened, read, written, closed, etc. Similarly, in the network, we can also perform such operations on Socket sockets, such as opening a network connection, reading data sent by the connected host, sending data to the connected host, terminating the connection, and other operations.

Sockets in LwIP

In LwIP, Socket API is implemented based on NETCONN API. The system provides a maximum of MEMP_NUM_NETCONN netconn connection structures, so the number of Socket sockets is also so many. In order to better encapsulate netconn, LwIP also defines a The socket structure - lwip_sock (I call it the Socket connection structure), each lwip_sock has a netconn pointer inside, which realizes the re-encapsulation of netconn, so how to find the lwip_sock structure? LwIP defines a sockets array of lwip_sock type, which can be directly indexed and accessed through the socket, which is why the socket is an integer. The lwip_sock structure is relatively simple, because basically it is all It depends on the implementation of netconn, see the code list for details.

Socket-related data structures in the code list LwIP

#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;
	/** 收到数据的次数由event_callback() 记录,下面的字段在select 机制上使用*/
	s16_t rcvevent;
	/** 发送数据的次数,也是由回调函数记录的*/
	u16_t sendevent;
	/** Socket 上的发生的错误次数*/
	u16_t errevent;
	/** 使用select 等待此套接字的线程数*/
	SELWAIT_T select_waiting;
};

Socket API

socket()

The function of this function is to apply for a socket from the kernel. In essence, this function encapsulates the netconn_new() function. Although it is not called directly, the work done by the main body is done by the netconn_new() function. , and the function is essentially a macro definition, see the code list for details.

Code listing 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 服务类型(TCP/UDP/RAW) */
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define SOCK_RAW 3

The parameter domain indicates the protocol cluster used by the socket, and for the TCP/IP protocol, the value is always AF_INET.

The parameter type specifies the service type used by the socket, and there are 3 possible types:

  1. SOCK_STREAM: Provide reliable (that is, ensure that data is correctly transmitted to the other party) connection-oriented Socket service, mostly used for data (such as file) transmission, such as TCP protocol.
  2. SOCK_DGRAM: It provides unguaranteed message-oriented Socket service, mainly used to send broadcast information on the network, such as UDP protocol, providing connectionless and unreliable datagram delivery service.
  3. SOCK_RAW: Represents a raw socket, which allows applications to access raw data packets at the network layer. This socket is rarely used, so ignore it for now.

The parameter protocol specifies the protocol used by the socket. In IPv4, only the TCP protocol provides the reliable service SOCK_STREAM, and only the UDP protocol provides the SOCK_DGRAM service. For both protocols, the value of protocol is 0.

When applying for a socket successfully, the function returns an int type value, which is also a Socket descriptor, through which the user can index to a Socket connection structure - lwip_sock; when applying for a socket fails, the function returns - 1.

bind()

The function of this function is the same as the netconn_bind() function, which is used to bind the socket and network card information on the server side. For binding, see the code list for the function prototype

Code listing bind()

#define bind(s,name,namelen) \
					lwip_bind(s,name,namelen)
					
int lwip_bind(int s,
			const struct sockaddr *name,
			socklen_t namelen);

The parameter s is the Socket to be bound. Note that this socket must be the index returned from the socket() function, otherwise the binding operation will not be completed.

The parameter name is a pointer to the sockaddr structure, which contains important information such as the IP address and port number of the network card. In order to better describe these information, LwIP uses the sockaddr structure to define the necessary information fields. It often It is used in many functions of Socket API, when we use bind(), we only need to directly fill in the relevant fields, see the code list for details of the sockaddr structure.

The parameter namelen specifies the length of the name structure.

Code list sockaddr structure

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

At first glance at this structure, it seems that there is no information for us to fill in, and it is indeed the case. The information we need to fill in, such as the IP address and port number, is in the continuous 14-byte information of sa_data, but this data is not useful to us. Friendly, so LwIP also defines another structure that is more friendly to developers - sockaddr_in, we generally use this structure, see the code list for details

Code list sockaddr_in structure

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];
};

The first two fields of this structure are consistent with the first two fields of the sockaddr structure, and the remaining fields are the contents of the continuous 14-byte information of sa_data, but the member variables are redefined. The sin_port field is our The port number information that needs to be filled in, the sin_addr field is the IP address information that we need to fill in, and the remaining 8 bytes in the sin_zero area are reserved for use.

So how should this function be used? See the code listing for details,

How to use the bind() function in the code list

sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
    
    
	printf("Socket error\n");
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(5001);
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");
}

connect()

The function of this function is basically the same as that of the netconn_connect() function, because it encapsulates the netconn_connect() function. It is used in the client to bind the Socket with the remote IP address and port number. In the TCP client connection, calling this function will cause a handshake process (a TCP connection request will be sent), and finally establish a new TCP For the UDP protocol, calling this function just records the remote IP address and port number in the UDP control block without sending any data. The parameter information is the same as that of the bind() function, see the code list for details.

Code listing connect()

#define connect(s,name,namelen) \
							lwip_connect(s,name,namelen)

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

listen()

This function is the encapsulation of the netconn_listen() function, which can only be used in the TCP server, so that the server enters the listening state and waits for the connection request from the remote end. LwIP can receive connections from multiple clients, so the parameter backlog specifies the request queue. For size, see the code listing for details.

Code list listen()

#define listen(s,backlog) \
				lwip_listen(s,backlog)
int
lwip_listen(int s, int backlog);

accept()

The accept() function has the same function as the netconn_accept() function. It is used in the TCP server, waiting for the connection request from the remote host, and establishing a new TCP connection. Before calling this function, the server needs to call the listen() function to enter monitoring status. The call to the accept() function blocks the application thread until a TCP connection is established with the remote host. The parameter addr is a return result parameter. Its value is set by the accept() function. In fact, it is the address and port number of the remote host. When a new connection is established, the information of the remote host will be saved in the connection handle. It can uniquely identify a connection object. At the same time, the function returns a socket descriptor of int type, according to which it can index to the connection structure, if the connection fails, it returns -1, see the code list for details.

Code list accept()

#define accept(s,addr,addrlen) \
			lwip_accept(s,addr,addrlen)

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

read()、recv()、recvfrom()

The core of the read() and recv() functions is to call the recvfrom() function, and the recvfrom() function is implemented based on the netconn_recv() function. The recv() and read() functions are used to receive data from the Socket, and they can It is the TCP protocol and the UDP protocol, see the code list for details.

Code Listing read(), recv(), recvfrom()

#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);
}

#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);
}

#define recvfrom(s,mem,len,flags,from,fromlen) \
		lwip_recvfrom(s,mem,len,flags,from,fromlen)
ssize_t
lwip_recvfrom(int s, void *mem, size_t len, int flags,
			struct sockaddr *from, socklen_t *fromlen)

The men parameter records the buffer start address of the received data, len is used to specify the maximum length of the received data, if the function can receive the data correctly, it will return a length of the received data, otherwise it will return -1, if the return value is 0, indicating that the connection has been terminated, and the application can perform different operations according to the returned value. The recv() function contains a flags parameter, we can ignore it for now and set it to 0. Note that if the received data is larger than the buffer provided by the user, the excess data will be discarded directly.

sendto()

This function is mainly used in the UDP protocol to transmit data. It sends a UDP message to the UDP host at the other end. It is essentially an encapsulation of the netconn_send() function. The parameter data specifies the starting address of the data to be sent, and the size Then specify the length of the data. The parameter flag specifies some processing when sending, such as external data, etc. At this time, we don’t need to pay attention to it. Generally, it can be set to 0. The parameter to is a pointer to the sockaddr structure, here We need to provide the IP address and port number of the remote host by ourselves, and use the tolen parameter to specify the length of the information, see the code list for details.

Code listing sendto()

#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)

send()

The send() function can be used for UDP protocol and TCP connection to send data. Before calling the send() function, you must use the connect() function to bind the IP address and port number of the remote host to the Socket connection structure. For the UDP protocol, the send() function will call the lwip_sendto() function to send data, and for the TCP protocol, it will call the netconn_write_partly() function to send data. Compared with the sendto() function, the parameters are basically the same, but we don’t need to set the information of the remote host, which is more convenient to operate, so this function is used a lot in practice, see the code for details.

Code listing send()

(续上页)
ssize_t
lwip_send(int s, const void *data, size_t size, int flags)

write()

This function is generally used to transmit data in a stable TCP connection. Of course, it can also be used in UDP protocol. It is also implemented based on lwip_send, but we don't need to set the flag parameter. See the code list for details.

Code listing write()

#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);
}

close()

The close() function is used to close a specified socket. After closing the socket, the corresponding socket descriptor cannot be used to index to the connection structure. The essence of this function is the encapsulation of the netconn_delete() function ( The actual processing function is netconn_prepare_delete()), if the connection is TCP protocol, it will generate a message requesting to terminate the connection and send it to the peer host, if it is UDP protocol, it will directly release the content of UDP control block, see the code list for details .

Code listing close()

#define close(s) \
		lwip_close(s)

int
lwip_close(int s)

ioctl()、ioctlsocket()

These two functions are very interesting (in fact, they are the same, the essence is a macro definition, and both call the lwip_ioctl() function), which are used to obtain the operation parameters related to setting the socket, and the parameter cmd indicates the operation command for the socket , only FIONREAD and FIONBIO commands are supported in LwIP:

  1. The FIONREAD command determines the amount of data automatically read by the socket s. These data have been received but not read by the application thread, so this function can be used to obtain the length of these data. In this command state, the argp parameter points to An unsigned long integer used to save the return value of the function (that is, the length of the unread data). If the socket is of SOCK_STREAM type, the FIONREAD command will return the amount of all data received in the recv() function, which is usually the same as the total amount of data queued in the socket receive buffer queue; and if the socket is SOCK_DGRAM type, the FIONREAD command will return the size of the first packet queued in the socket receive buffer queue.
  2. The FIONBIO command is used to enable or disable non-blocking mode for sockets. Under this command, the argp parameter points to an unsigned long integer. If the value is 0, it means that the non-blocking mode is prohibited, and if the value is not 0, it means that the non-blocking mode is allowed. When a socket is created, it is in blocking mode, which means that non-blocking mode is prohibited. In this case, all sending and receiving functions will be blocked until the sending and receiving are successful; and If it is in non-blocking mode, all sending and receiving functions are non-blocking. If the data cannot be sent or received, an error code will be returned directly to the user. This requires the user to deal with these "unexpected" situations to ensure that The robustness of the code, which is consistent with BSD Socket.

See the code listing for the function prototype.

Code Listing ioctl(), ioctlsocket()

#define ioctl(s,cmd,argp) \
			lwip_ioctl(s,cmd,argp)
			
#define ioctlsocket(s,cmd,argp) \
			lwip_ioctl(s,cmd,argp)

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

setsockopt()

As you can see from the name, this function is used to set some options of the socket. The parameter level has many common options, such as:

  • SOL_SOCKET: Indicates at the Socket layer.
  • IPPROTO_TCP: Indicates at the TCP layer.
  • IPPROTO_IP: Indicates at the IP layer.

The parameter optname indicates the specific option name of this layer, for example:

  1. For the SOL_SOCKET option, it can be SO_REUSEADDR (allow reuse of local address and port), SO_SNDTIMEO (set send data timeout), SO_SNDTIMEO (set receive data timeout), SO_RCVBUF (set send data buffer size) and so on.
  2. For the IPPROTO_TCP option, it can be TCP_NODELAY (do not use Nagle's algorithm), TCP_KEEPALIVE (set TCP keep-alive time), and so on.
  3. For the IPPROTO_IP option, it can be IP_TTL (set time to live), IP_TOS (set type of service), etc.

Code Listing setsockopt()

#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)

getsockopt()

The option parameters and names of this function are similar to the setsockopt() function, but the function is to obtain the information of these options, so I won’t explain too much here.

experiment

TCP Client

This experimental phenomenon is the same as the experiment in the NETCONN API. We directly copy the last project, and then replace the NETCONN API with the Socket API, which is basically the same. We first configure the macro LWIP_SOCKET to 1 in the lwipopts.h file , add the following code in the file, be careful not to delete the LWIP_NETCONN macro definition.

#define LWIP_SOCKET 1

Add the code shown in the code list to the client.c file. Of course, the port number and other information can be modified according to your own network environment. Then compile the project and download it to the development board. The operation steps on the computer side are the same as the experimental operation steps in the NETCONN API It is the same, so I won't repeat it too much.

Code list client.c file content

#include "client.h"

#include "lwip/opt.h"

#include "lwip/sys.h"
#include "lwip/api.h"

#include <lwip/sockets.h>
#define PORT 5001
#define IP_ADDR "192.168.0.181"

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);
	}
}

void
client_init(void)
{
    
    
	sys_thread_new("client", client, NULL, 512, 4);
}

TCP Server

Similarly, this experiment only needs to copy the experiment in the NETCONN API, then configure the macro LWIP_SOCKET to 1 in the lwipopts.h file, and then replace the content of the tcpecho.c file with the content of the code list. The operation steps The same is true, then compile the project and download it to the development board to see the experimental phenomenon.

Code list tcpecho.c file content

#include "tcpecho.h"

#include "lwip/opt.h"

#if LWIP_SOCKET
#include <lwip/sockets.h>

#include "lwip/sys.h"
#include "lwip/api.h"
/*--------------------------------------------------------------------*/

#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);
}

void
tcpecho_init(void)
{
    
    
	sys_thread_new("tcpecho_thread", tcpecho_thread, NULL, 512, 4);
}

UDP

Similarly, this experiment only needs to copy the experiment in the NETCONN API, then configure the macro LWIP_SOCKET to 1 in the lwipopts.h file, and then replace the content of the udpecho.c file with the content of the code list. The operation steps The same is true, then compile the project and download it to the development board to see the experimental phenomenon.

Code list udpecho.c file content

#include "udpecho.h"

#include "lwip/opt.h"

#include <lwip/sockets.h>
#include "lwip/api.h"
#include "lwip/sys.h"

#define PORT 5001

#define RECV_DATA (1024)
/*-------------------------------------------------------------*/
static void
udpecho_thread(void *arg)
{
    
    
	int sock = -1;
	char *recv_data;
	struct sockaddr_in udp_addr,seraddr;
	int recv_data_len;
	socklen_t addrlen;
	
	while (1)
	{
    
    
		recv_data = (char *)pvPortMalloc(RECV_DATA);
		if (recv_data == NULL)
		{
    
    
			printf("No memory\n");
			goto __exit;
		}
		
		sock = socket(AF_INET, SOCK_DGRAM, 0);
		if (sock < 0)
		{
    
    
			printf("Socket error\n");
			goto __exit;
		}
		
		udp_addr.sin_family = AF_INET;
		udp_addr.sin_addr.s_addr = INADDR_ANY;
		udp_addr.sin_port = htons(PORT);
		memset(&(udp_addr.sin_zero), 0, sizeof(udp_addr.sin_zero));

		if (bind(sock, (struct sockaddr *)&udp_addr, sizeof(struct sockaddr)) == -1)
		{
    
    
			printf("Unable to bind\n");
			goto __exit;
		}
		while (1)
		{
    
    
			recv_data_len=recvfrom(sock,recv_data,
								RECV_DATA,0,
								(struct sockaddr*)&seraddr,
								&addrlen);
								
			/* 显示发送端的IP 地址*/
			printf("receive from %s\n",inet_ntoa(seraddr.sin_addr));
			/* 显示发送端发来的字串*/
			printf("recevce:%s",recv_data);
			
			/* 将字串返回给发送端*/
			sendto(sock,recv_data,
				recv_data_len,0,
				(struct sockaddr*)&seraddr,
				addrlen);
		}
		
__exit:
		if (sock >= 0) closesocket(sock);
		if (recv_data) free(recv_data);
	}
}

/*---------------------------------------------------------------------*/
void
udpecho_init(void)
{
    
    
	sys_thread_new("udpecho_thread", udpecho_thread, NULL, 2048, 4);
}

Reference: LwIP Application Development Practical Guide - Based on Wildfire STM32

Guess you like

Origin blog.csdn.net/picassocao/article/details/129274277