Page 1 of 4
What is the simplest HTTP client you can create using lwIP? The answer is very simple indeed. This is an extract from our latest book on the Pico in C.
Master the Raspberry Pi Pico in C:
WiFiwith lwIP & mbedtls
By Harry Fairhead & Mike James
Buy from Amazon.
- The Pico WiFi Stack
- Introduction To TCP
Extract: Simplest HTTP Client
- More Advanced TCP
- SSL/TLS and HTTPS
Extract: Simplest HTTPS Client NEW!!
- Details of Cryptography
- UDP For Speed
- SNTP For Time-Keeping
- SMTP For Email
- MQTT For The IoT
Appendix 1 Getting Started In C
In book but not in this extract
- Introduction To TCP
- Ethernet, IP, TCP and HTTP
We could start at a lower level in the stack, but TCP is the most commonly encountered transport protocol and it is used to implement web clients and web servers among other things, so it is a good practical level to start.
A TCP data exchange has to be made via a connection made by a client to a server. The client asks to connect to the server and the server accepts or rejects the connection. Once the connection has been made data can flow from client to server or vice versa. Connections are specified and controlled using a PCB, Protocol Control Block. What usually happens is that you first create a PCB and then use this to accept or make a connection. Once you have a connection data can be sent and received. The PCB is used to keep track of the state of the connection, including storing any data while it is being transferred and to customize the TCP protocol.
Creating a PCB
To create a PCB you use:
Next you have to initialize the pcb. It has a set of fields that record the callbacks that are used to service the data transfer. You need to use access functions to set the callbacks rather than setting the fields directly.
The sent function is called when the remote side of the connection has acknowledged the data sent to it. You can use this to free resources and to detect when the PCB has freed up space in its data storage and you can send more data. The signature of the sent function has to be:
err_t sent(void *arg, struct pcb *pcb, u16_t len)
where pcb is the PCB that sent the data and len is the number of bytes sent. The first parameter, arg, is a custom argument that you can specify to be sent to the callback and it is included in all of the callbacks.
The recv function is called when data is ready to be proceeded by the client.
The signature of the recv function is:
errr_t recv(void *arg, struct tcp_pcb *pcb,
struct pbuf *p, err_t err)
where pcb is the PCB that received the data, p is a PBUF which contains the data and err_t is an error code associated with the received data and used if there was a transmission error. The PBUF is a data structure in the form of a linked list that holds the packets transmitted over the connection. There are some utility functions that make working with a PBUF easy, see later. However, you should call:
after the received data has been retrieved and is no longer wanted in order to free up the len bytes in the PCB for more data.
The err function called if there is any error associated with the PCB. Not all errors result in this function being called, some are reported to a more appropriate callback, but it is called when a connection receives a RST (reset) instruction from the server or when it is unexpectedly closed.
Making the Connection
The PCB is all about implementing the connection, but it isn’t involved in determining where the connection is to. Exactly how the connection is made depends on whether you are the client or the server. For simplicity we will deal with how a client connects first and come back to the problem of handling server connections.
A client connection is made using the tcp_connect function. This is where you specify the IP address and port number for the connection:
err_t tcp_connect(struct tcp_pcb * pcb,
const ip_addr_t * ipaddr, u16_t port,
tcp_connected_fn connected )
The pcb is the PCB used for the connection and hence it specifies the callback functions. The ipaddr and port specify the server and the connected function is called when the connection is made.
Its signature is:
err_t connected(void *arg, struct _pcb *pcb, err_t err)
where pcb specifies the PCB used in the connection.
The err parameter is currently unused and it is always ERR_OK. If there is an error the error callback set up in the PCB is called. The first parameter, arg, is a custom argument that you can set to be passed to all callbacks associated with the PCB.
Once you have an open connection you can send data to the server using:
err_t tcp_write(struct tcp_pcb *pcb,
const void * data, u16_t len u8_t apiflags)
where pcb specifies the PCB to use, data is a pointer to the data to be sent and len specifies the number of bytes in the data buffer.
Finally apiflags specifies how the data should be treated and can be
specifies that the data buffer should be copied into internal stack memory. If you don’t specify this the buffer that you supply should not be changed until the data has been transmitted and acknowledged.
indicates that more data follows.
The amount of data sent should not exceed the limit defined in the lwipopts.h file. If there isn’t enough memory the function returns ERR_MEM. You can find out the size of the internal buffer using the tcp_sndbuf() macro.
The data isn’t always sent immediately and you should call:
err_t tcp_output(struct tcp_pcb * pcb)
specifying the appropriate PCB to ensure it is sent.
There are other useful TCP functions but these are all we need to write a simple HTTP client. The basic algorithm is:
Create and initialize a PCB with at least a recv callback defined
Use tcp_connect to connect to a specified IP and port address and supply a connected callback
Create the connected callback to send data to the server and create the recv callback to process the data that the server sends back in response.