14丨UDP can also be “connected”?

In the previous basics, we have been exposed to knowledge related to the UDP datagram protocol, and the characteristics of “UDP equals connectionless protocol” have been deeply imprinted in our minds. So when you see the title of this lecture, do you feel a little confused? It doesn’t matter, come with me into the world of “connected” UDP, look back at this title, I believe you will suddenly understand.

Start with an example

Let’s start with a client example. In this example, the client calls the connect function on the UDP socket, then sends the standard input string to the server and receives the processed message from the server. Of course, sending and receiving messages to the server is accomplished by calling the functions sendto and recvfrom.

#include "lib/common.h"
#defineMAXLINE 4096

int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: udpclient1 <IPaddress>");
    }

    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in server_addr;
    bzero( & amp;server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], & amp;server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);

    if (connect(socket_fd, (struct sockaddr *) & amp;server_addr, server_len)) {
        error(1, errno, "connect failed");
    }

    struct sockaddr *reply_addr;
    reply_addr = malloc(server_len);

    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    socklen_t len;
    int n;

    while (fgets(send_line, MAXLINE, stdin) != NULL) {
        int i = strlen(send_line);
        if (send_line[i - 1] == '\\
') {
            send_line[i - 1] = 0;
        }

        printf("now sending %s\\
", send_line);
        size_t rt = sendto(socket_fd, send_line, strlen(send_line), 0, (struct sockaddr *) & amp;server_addr, server_len);
        if (rt < 0) {
            error(1, errno, "sendto failed");
        }
        printf("send bytes: %zu \\
", rt);
        
        len = 0;
        recv_line[0] = 0;
        n = recvfrom(socket_fd, recv_line, MAXLINE, 0, reply_addr, & amp;len);
        if (n < 0)
            error(1, errno, "recvfrom failed");
        recv_line[n] = 0;
        fputs(recv_line, stdout);
        fputs("\\
", stdout);
    }

    exit(0);
}

Let me give a simple explanation of this program:

Lines 9-10 create a UDP socket;

Lines 12-16 create an IPv4 address bound to the specified port and IP;

Lines 20-22 call connect to “bind” the UDP socket and the IPv4 address. The name of the connect function here is a bit misleading. In fact, a better choice may be to call it setpeername;

Lines 31-55 are the main body of the program. After reading the standard input string, call sendto to send it to the peer; then call recvfrom to wait for the peer’s response and print the peer’s response information to the standard output.

Without starting the server, let’s run this program:

$ ./udpconnectclient 127.0.0.1
g1
now sending g1
send bytes: 2
recvfrom failed: Connection refused (111)

Do you feel strange when you see this? Isn’t it said that UDP is a “connectionless” protocol? Isn’t it said that the UDP client will only block on calls like recvfrom? Why is there a “Connection refused” error here?

Don’t worry, just follow my thoughts and slowly solve this mystery.

The role of UDP connect

From the previous example, you will find that we can call the connect function on the UDP socket, but unlike the TCP connect call, which causes the TCP three-way handshake and establishes a valid TCP connection, the UDP connect function call will not cause the connection with the server target. End-to-end network interaction, that is to say, does not trigger the so-called “handshake” message transmission and response.

So what is the point of connecting a UDP socket?

In fact, the above example has already given the answer. This is mainly to enable the application to receive “asynchronous error” information.

If we recall the client program that does not call the connect operation in Part 6, the client program will not report an error when the server is not turned on. The program will only block on recvfrom, waiting for return (or timeout).

Here, we establish a “context” for the UDP socket by performing a connect operation on the UDP socket. The socket is connected to the address and port of the server. It is this binding relationship that gives the operating system Necessary information for the kernel to correlate information received by the operating system kernel with the corresponding socket.

We can discuss it.

In fact, when we call the sendto or send operation function, the application message is sent, our application returns, the operating system kernel takes over the message, and then the operating system starts trying to send to the corresponding address and port, because the corresponding If the address and port are unreachable, an ICMP message will be returned to the operating system kernel. The ICMP message contains information such as the destination address and port.

If we do not perform the connect operation and establish the mapping relationship between (UDP socket – destination address + port), the operating system kernel will have no way to associate the ICMP unreachable information with the UDP socket, and there will be no way Notify applications of ICMP messages.

If we perform the connect operation, it helps the operating system kernel to easily establish the mapping relationship between (UDP socket – destination address + port). When an ICMP unreachable message is received, the operating system kernel can retrieve the mapping from the mapping table. Find out which UDP socket owns the destination address and port. Don’t forget that the socket is globally unique within the operating system. When we call the recvfrom or recv method again on the socket, we can receive “Connection Refused” information returned to the operating system kernel.

Transmitting and receiving functions

After connecting UDP, many books recommend the use of sending and receiving functions as follows:

Use the send or write function to send. If you use sendto, you need to set the relevant to address information to zero;

Use the recv or read function to receive. If you use recvfrom, you need to set the corresponding from address information to zero.

In fact, different UNIX implementations behave differently in this regard.

In my Linux 4.4.0 environment, using sendto and recvfrom, the system automatically ignores to and from information. On my macOS 10.13, which does require compliance, I get some weird results using sendto or recvfrom, which works fine after switching back to send and recv.

We also recommend these general practices for compatibility reasons. So in the next program, I will use this approach to achieve it.

Example of server-side connect

Generally speaking, the server will not actively initiate a connect operation, because once it does, the server can only respond to one client. However, sometimes it is not ruled out that once a client and server send UDP messages, the server must serve the only client.

A similar server-side program is as follows:

#include "lib/common.h"

static int count;

static void recvfrom_int(int signo) {
    printf("\\
received %d datagrams\\
", count);
    exit(0);
}

int main(int argc, char **argv) {
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in server_addr;
    bzero( & amp;server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);

    bind(socket_fd, (struct sockaddr *) & amp;server_addr, sizeof(server_addr));

    socklen_t client_len;
    char message[MAXLINE];
    message[0] = 0;
    count = 0;

    signal(SIGINT, recvfrom_int);

    struct sockaddr_in client_addr;
    client_len = sizeof(client_addr);

    int n = recvfrom(socket_fd, message, MAXLINE, 0, (struct sockaddr *) & amp;client_addr, & amp;client_len);
    if (n < 0) {
        error(1, errno, "recvfrom failed");
    }
    message[n] = 0;
    printf("received %d bytes: %s\\
", n, message);

    if (connect(socket_fd, (struct sockaddr *) & amp;client_addr, client_len)) {
        error(1, errno, "connect failed");
    }

    while (strncmp(message, "goodbye", 7) != 0) {
        char send_line[MAXLINE];
        sprintf(send_line, "Hi, %s", message);

        size_t rt = send(socket_fd, send_line, strlen(send_line), 0);
        if (rt < 0) {
            error(1, errno, "send failed ");
        }
        printf("send bytes: %zu \\
", rt);

        size_t rc = recv(socket_fd, message, MAXLINE, 0);
        if (rc < 0) {
            error(1, errno, "recv failed");
        }
        
        count + + ;
    }

    exit(0);
}

Let me explain this procedure:

Lines 11-12 create the UDP socket;

Lines 14-18 create an IPv4 address and bind it to ANY and the corresponding port;

Line 20 binds UDP socket and IPv4 address;

27 Register a signal handling function for this program to respond to Ctrl + C semaphore operation;

Lines 32-37 call recvfrom to wait for the client message to arrive and save the client information to client_addr;

Lines 39-41 call the connect operation to bind the UDP socket to the client client_addr;

Lines 43-59 are the main body of the program, which reprocesses the received information, adds “Hi” prefix and sends it to the client, and continuously receives messages from the client. This process continues until the client sends ” goodbye” message.

Note that all send and receive functions here use send and recv.

Next we implement a connect client program:

#include "lib/common.h"
#defineMAXLINE 4096

int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: udpclient3 <IPaddress>");
    }

    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in server_addr;
    bzero( & amp;server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], & amp;server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);

    if (connect(socket_fd, (struct sockaddr *) & amp;server_addr, server_len)) {
        error(1, errno, "connect failed");
    }

    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    int n;

    while (fgets(send_line, MAXLINE, stdin) != NULL) {
        int i = strlen(send_line);
        if (send_line[i - 1] == '\\
') {
            send_line[i - 1] = 0;
        }

        printf("now sending %s\\
", send_line);
        size_t rt = send(socket_fd, send_line, strlen(send_line), 0);
        if (rt < 0) {
            error(1, errno, "send failed ");
        }
        printf("send bytes: %zu \\
", rt);

        recv_line[0] = 0;
        n = recv(socket_fd, recv_line, MAXLINE, 0);
        if (n < 0)
            error(1, errno, "recv failed");
        recv_line[n] = 0;
        fputs(recv_line, stdout);
        fputs("\\
", stdout);
    }

    exit(0);
}

Let me interpret this client program:

Lines 9-10 create a UDP socket;

Lines 12-16 create an IPv4 address bound to the specified port and IP;

Line 20-22 calls connect to “bind” the UDP socket and IPv4 address;

Lines 27-46 are the main body of the program. After reading the standard input string, call send to send it to the peer; then call recv to wait for the peer’s response and print the peer’s response information to the standard output.

Note that all send and receive functions here also use send and recv.

Next, we start the server program first, and then start two clients in sequence, namely client 1 and client 2, and let client 1 send UDP messages first.

Service-Terminal:

$ ./udpconnectserver
received 2 bytes: g1
send bytes: 6

Client 1:

 ./udpconnectclient2 127.0.0.1
g1
now sending g1
send bytes: 2
Hi, g1

Client 2:

./udpconnectclient2 127.0.0.1
g2
now sending g2
send bytes: 2
recv failed: Connection refused (111)

We see that client 1 sends the message first, and the server then “binds” with client 1 through connect. In this way, client 2 gets an ICMP error from the operating system kernel, which is in the recv function. Return, the error message “Connection refused” is displayed.

Performance considerations

Generally speaking, the client binds the address and port of the server through connect, which can improve performance to a certain extent for UDP.

Why is this?

Because if you don’t use the connect method, this process will be required every time you send a message:

Connect socket → Send message → Disconnect socket → Connect socket → Send message → Disconnect socket →…………

And if you use the connect method, it will become like this:

Connect the socket→Send the message→Send the message→…→Finally disconnect the socket

We know that connecting to a socket requires a certain amount of overhead, such as looking up routing table information. Therefore, the UDP client program can obtain certain performance improvements through connect.

Summary

In today’s content, I conducted an in-depth analysis of UDP socket calling connect method. The reason why we use connect for UDP and bind the local address and port is so that our program can quickly obtain notifications of asynchronous error information, and at the same time, we can also obtain certain performance improvements.

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Network skill treeCommunication learning in cross-regional networksThe role of the network layer 41873 people are learning the system