Parsing the key points of C programming based on the UDP protocol

  • 2020-05-09 18:53:46
  • OfStack

Two protocols, TCP and UDP
The former can be understood as a guaranteed connection, while the latter is the pursuit of fast connection.
Of course the last point is a little too absolute, but you don't have to worry too much about it right now, because in the beginning of socket programming, the first cut is simple.
If you think about it for a moment, you can understand that TCP is looking for reliable data transmission, while UDP is looking for fast data transmission.
The former has a cumbersome connection process, while the latter does not establish a reliable connection at all (not absolutely) and simply sends the data regardless of whether it arrives or not.
The following example takes the *nix platform as an example, because the Windows platform needs to consider additional loading issues, so you can run UDP on the Windows platform with a few additions.

UDP

This is a 10-point simple connection, assuming that there are two hosts for communication, 1 only send, 1 only receive.
The receiver:


  int sock; /*  The socket  */
  socklen_t addr_len; /*  The address length of the sender, used for  recvfrom */
  char mess[15];
  char get_mess[GET_MAX]; /*  Use of subsequent versions  */
  struct sockaddr_in recv_host, send_host;

  /*  Create a socket  */
  sock = socket(PF_INET, SOCK_DGRAM, 0);

  /*  the IP  and   The port number information is bound to the socket  */
  memset(&recv_host, 0, sizeof(recv_host));
  recv_host.sin_family = AF_INET;
  recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/*  Receive arbitrary IP */
  recv_host.sin_port = htons(6000); /*  use 6000  The port number  */
  bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));

  /*  Enter the receiving state  */
  recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);

  /*  Close the socket after receiving  */
  close(sock);

The code above omits many of the necessary error checks to be added when you actually write it

Code explanation:
PF_INET stands for the type of protocol, here for the IPv4 network protocol family, and PF_INET6 for the IPv6 network protocol family. This range is recorded separately in the back and does not mix with IPv4 in 1 (does not mean more complex, in fact more convenient).
AF_INET represents the type of address, which in this case represents the address family used by the IPv4 network protocol, as well as AF_INET6 (in the operating system implementation PF_INET and AF_INET values 1, but it is better to write macros instead of using Numbers directly or obfuscating them).
The use of the htonl and htons functions is related to the big-end and small-end problem, which is not described here, but it is important to remember that this function is always used in network programming to translate the necessary information into the big-end representation.
(struct sockaddr *) this cast is required for parameter purposes, but it should not go wrong, because sizeof(struct sockaddr_in) == sizeof(struct sockaddr) can specifically query the relevant information, which is done for the convenience of the programmer writing the socket program.
The sender:


  int sock;
  const char* mess = "Hello Server!";
  char get_mess[GET_MAX]; /*  Use of subsequent versions  */
  struct sockaddr_in recv_host;
  socklen_t addr_len;
  /*  Create a socket  */
  sock = socket(PF_INET, SOCK_DGRAM, 0);
  /*  The binding  */
  memset(&recv_host, 0, sizeof(recv_host));
  recv_host.sin_family = AF_INET;
  recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
  recv_host.sin_port = htons(6000);
  /*  Send a message  */
  /*  Here, the sender IP All kinds of information, such as address and port number, are automatically bound to the socket as the function is called  */
  sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
  /*  Done, closed  */
  close(sock);

The above code is the sending end.

Code explanation:
The inet_addr function is used to convert the IP address in string format to the big-end representation of the address type, in_addr_t of s_addr
In contrast, there is also the opposite function inet_ntoa, which converts in_addr_t to a string, but remember to copy the return value char addr[16]; recv_host. sin_addr. s_addr = inet_addr (" 127.0.0.1 "); strcpy (addr inet_ntoa (recv_host. sin_addr. s_addr));
As you can see from the code above, the use of the UDP protocol is 10 minutes concise, almost creating sockets - > Prepare data - > Equip socket - > Send/receive - > The end of the
There is no connection operation, but in fact this is the default setting for UDP to communicate with different hosts at any time. What if you need to communicate directly with the same host 1?
The reason for this need not be known for a while, the recording method, that is, when using UDP and communicating with the same host for a long time, can use the connect function to optimize itself. At this point, the actual function of the two hosts is assumed to be 1, both receiving and sending
The sender:


  /*  In front of the highly 1 Will,  bind Replace the function with  */
  connect(sock, (struct sockaddr *)&recv_host, sizeof(recv_host); //  To the other side of the  IP The address and   Port number information   Registered in UDP In a socket )
  while(1) /*  The loop sends and receives information  */
  {
   size_t read_len = 0;
   /*  Originally used  sendto  Function, first choose to use  write  The function,  Windows Platform for  send  function  */
   write(sock, mess, strlen(mess));      /* send(sock, mess, strlen(mess), 0) FOR Windows Platform */
   read_len = read(sock, get_mess, GET_MAX-1); /* recv(sock, mess, strlen(mess)-1, 0) FOR Windows Platform */
   get_mess[read_len-1] = '\0';
   printf("In Client like Host Recvive From Other Host : %s\n", get_mess);
  }
  /*  The rear height 1 to  */

The receiver:


  /*  In front of the 1 Cause,   Add extra  struct sockaddr_in send_host;  And add a loop to construct the transceiver phenomenon */
    while(1)
  {
   size_t read_len = 0;
   char sent_mess[15] = "Hello Sender!"; /*  The information used to send  */
   sendto(sock, mess, strlen(sent_mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
   read_len = recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len)
   mess[read_len-1] = '\0';
   printf("In Sever like Host Recvive From other Host : %s\n", mess);
  }
  /*  The rear height 1 to  */
  /*
  *  It is only used on the receiving end  connect  The reason is that what we simulate is   The client - The server   The model, and the server's information is not arbitrary changes 
  *  but   The client side is different, probably because  ISP(Internet Server Provider)  Cause of yours IP The address cannot always be fixed, so it can only be 
  *  ensure   On the client side   Partially registered   The server   All kinds of information, but not in   The server side   registered   The client   The information. 
  *  There are exceptions, for example, if you think of it as private software for two people,   And you have fixed  IP Address, then you can go both ways connect , but 
  * 1 Be careful, as long as there is 1 Point information changes, the software may not be able to send and receive messages normally. 
  */

Code interpretation
In fact, although the table above shows that UDP does not seem to be necessary for the use of connect, it is still useful.
For API of *nix, the difference between sendto and write is 10 points, that is, one needs to provide all kinds of information of the target host in the parameters, while the latter does not. The same goes for recvfrom and read.
This code is just for demonstration purposes, so put the code in an infinite loop and you can actually define your own exit conditions.
The above is a brief explanation of UDP 1, which is enough to get you started. It does not describe the specific use of certain functions in detail, but is based on actual examples. Before recording TCP, we still need to talk about one function, shutdown
shutdown and close (closesocket)

First of all, we should know that network communication 1 is generally carried out by both sides, in other words, it is bidirectional, 1 direction only used to send messages, 1 direction only used to read messages.
This results in the need to close channels in both directions (call them channels for the time being) at the end of socket communication. Why not close both channels at the same time? B: yes, you can
close (sock); / / closesocket (sock); That's what FOR Windows PlatForm does, disconnecting in both directions at the same time.
A simple communication program or a one-way communication program can do this without much harm, but what if $1 needs to receive the last message at the end of the communication?
Assuming the communication ends, the client sends "Thank you" to the server
The server needs to receive this information before it can shut down communication
The problem is that the server doesn't know when the client will send the message after the communication ends
Therefore, we choose to close the sending channel (write stream) of the server after the communication is completed, and close the remaining receiving channel (read stream) after waiting for the client to send a message.
The sender:


  /*  Suppose you have 1 a  TCP  This is the client side  */
  write(sock, "Thank you", 10);
  close(sock); //  Close communication after writing 

The receiver:


  /*  This is the server  */
  /*  Close the write stream first  */
  shutdown(sock_c, SHUT_WR);
  read(sock_c, get_mess, GET_MAX);
  printf("Message : %s\n", get_mess);
  close(sock_c);
  close(sock_s); //  Close two sockets because  TCP  On the server side, it will be recorded later 

Code interpretation
The shutdown function is used to optionally close the output in that direction


int shutdown(int sock, int howto);

sock represents the socket to operate on
howto has several options

* nix ** : SHUT_RD SHUT_WR SHUT_RDWR Windows : SD_RECEIVE SD_SEND SD_BOTH


Below, there are several structures, as well as an interface 10 points important and commonly used:

struct sockaddr_in6: represents the address information of IPv6 struct addrinfo: this is a generic structure that can store information about addresses of type IPv4 or IPv6 getaddrinfo: this is a 10 point convenient interface, in the UDP program above many manually filled parts, can be omitted, have this function for us to complete

Rewrite 1 below the top example:

The receiver:


  int sock; /*  The socket  */
  socklen_t addr_len; /*  The address length of the sender, used for  recvfrom */
  char mess[15];
  char get_mess[GET_MAX]; /*  Use of subsequent versions  */
  struct sockaddr_in host_v4; /* IPv4  address  */
  struct sockaddr_in6 host_v6; /* IPv6  address  */
  struct addrinfo easy_to_use; /*  Used to set the information to be retrieved and how  */
  struct addrinfo *result;  /*  Used to store the information obtained ( Be aware of memory leaks ) */
  struct addrinfo * p;

  /*  To prepare information  */
  memset(&easy_to_use, 0, sizeof easy_to_use);
  easy_to_use.ai_family = AF_UNSPEC; /*  Tell the interface I don't know the address type yet  */
  easy_to_use.ai_flags = AI_PASSIVE; /*  Tell the interface that later "you" will help me fill in the information I didn't specify  */
  easy_to_use.ai_socktype = SOCK_DGRAM; /* UDP  The socket  */
  /*  The rest of the digits are zero  0 */

  /*  use  getaddrinfo  interface  */
  getaddrinfo(NULL, argv[1], &easy_to_use, &result); /* argv[1]  That holds the form of a string   The port number  */

  /*  Creating a socket, where you get two ways to write it, but it's safer to write it reliably  */
  /*  The old method 
  * sock = socket(PF_INET, SOCK_DGRAM, 0);
  */
  /*  the IP  and   The port number information is bound to the socket  */
  /*  The old method 
  * memset(&recv_host, 0, sizeof(recv_host));
  * recv_host.sin_family = AF_INET;
  * recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/*  Receive arbitrary IP */
  * recv_host.sin_port = htons(6000); /*  use 6000  The port number  */
  * bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
  */

  for(p = result; p != NULL; p = p->ai_next) /*  This syntax needs to be turned on  -std=gnu99  standard */
  {
    sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if(sock == -1)
     continue;
    if(bind(sock, p->ai_addr, p->ai_addrlen) == -1)
    {
     close(sock);
     continue;
    }
    break; /*  If you can do this, you can prove that the socket was successfully established and the socket binding was successful, so you don't have to try again.  */
  }

  /*  Enter the receiving state  */
  //recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
  switch(p->ai_socktype)
  {
   case AF_INET :
    addr_len = sizeof host_v4;
    recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v4, &addr_len);
    break;
    case AF_INET6:
     addr_len = sizeof host_v6
     recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v6, &addr_len);
     break;
    default:
     break;
  }
  freeaddrinfo(result); /*  Release this space by getaddrinfo The distribution of  */
  /*  Close the socket after receiving  */
  close(sock);

Code explanation:

First, explain a few new structures

The internal sequence of struct addrinfo is slightly different for *nix and Windows, taking *nix as an example


 struct addrinfo{
  int ai_flags;
  int ai_family;
  int ai_socktype;
  int ai_protocol;
  socklen_t ai_addrlen;
  struct sockaddr * ai_addr; /*  Where the result address is stored  */
  char * ai_canonname; /*  Ignore it. It's long 1 You don't have to pay attention to it for a while  */
  struct addrinfo * ai_next; /* 1 A domain name /IP The address may resolve to several different ones  IP */
 };

If ai_family is set to AF_UNSPEC then when you call getaddrinfo, it will automatically help you determine, what type of address is being passed in
If ai_flags is set to AI_PASSIVE, then when getaddrinfo is called and NULL is passed to its first parameter, IP is automatically bound to IP, which is equivalent to INADDR_ANY

ai_socktype is the socket type to be created, which must be explicitly declared and not anticipated by the system. Normally we set ai_protocol 1 to 0, and the meaning can be found by ourselves, such as MSDN or UNP ai_addr holds the result here, which can be obtained by calling the fourth parameter after getaddrinfo. ai_addrlen ditto ai_next ditto

getaddrinfo powerful interface functions

  int getaddrinfo(const char * node, const char * service,
                                      const struct addrinfo * hints, struct addrinfo ** res);
Say the function of these a few parameter colloquially
node is the domain name to be acquired or bound, or IP, that is, you can fill in the domain name directly, and the operating system will convert it to IP information, or you can fill in IP directly, in the form of a string
service means the port number, which is also a string
hints is generally used to tell the interface what information I need you to give me (parameter 4) and fill in the information in parameter 4.
res is where the result is saved. It should be noted that this result is dynamically allocated inside API, so it needs to be freed by calling another interface (freeaddrinfo) after use
In fact, for modern socket programming, there are several new structures for storing IP information, such as struct sockaddr_in6 and struct sockaddr_storage.

Where, the former is a subset of the size of the latter, that is, 1 struct storage 1 must be able to hold 1 struct sockaddr_in6, concrete (actually, there is no meaningful implementation)


  struct sockaddr_in6{
   u_int16_t sin6_family;
   u_int16_t sin6_port;
   u_int32_t sin6_flowinfo; /*  Ignore it for the moment  */
   struct in6_addr sin6_addr; /* IPv6  Is stored in this structure  */
   u_int32_t sin_scope_id; /*  Ignore it for the moment  */
  };
  struct in6_addr{
   unsigned char s6_addr[16];
  }
  ------------------------------------------------------------
  struct sockaddr_storage{
   sa_family_t ss_family; /*  Type of address  */
   char __ss_pad1[_SS_PAD1SIZE]; /*  From here on out, the non-implementer can hardly understand  */
   int64_t __ss_align;      /*  You can see from the name that it's probably for compatibility  IP  Type of compromise  */
   char __ss_pad2[_SS_PAD2SIZE]; /*  Hides the actual content, except  IP  It is not possible to obtain any other information directly beyond the category of.  */
   /*  In all *nix  In the concrete implementation of,   There may be different implementations, for example  `__ss_pad1`  .  `__ss_pad2` ,  May merge into 1 a  `pad`  .  */
  };

In practice, we often do not need to declare different storage types for different IP types, but simply use struct sockaddr_storage and cast the type directly when using it

In the overwrite receiver example, enter the status section of the received information


  int sock;
  const char* mess = "Hello Server!";
  char get_mess[GET_MAX]; /*  Use of subsequent versions  */
  struct sockaddr_in recv_host;
  socklen_t addr_len;
  /*  Create a socket  */
  sock = socket(PF_INET, SOCK_DGRAM, 0);
  /*  The binding  */
  memset(&recv_host, 0, sizeof(recv_host));
  recv_host.sin_family = AF_INET;
  recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
  recv_host.sin_port = htons(6000);
  /*  Send a message  */
  /*  Here, the sender IP All kinds of information, such as address and port number, are automatically bound to the socket as the function is called  */
  sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
  /*  Done, closed  */
  close(sock);
0

Complete the corresponding sender code above


  int sock;
  const char* mess = "Hello Server!";
  char get_mess[GET_MAX]; /*  Use of subsequent versions  */
  struct sockaddr_in recv_host;
  socklen_t addr_len;
  /*  Create a socket  */
  sock = socket(PF_INET, SOCK_DGRAM, 0);
  /*  The binding  */
  memset(&recv_host, 0, sizeof(recv_host));
  recv_host.sin_family = AF_INET;
  recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
  recv_host.sin_port = htons(6000);
  /*  Send a message  */
  /*  Here, the sender IP All kinds of information, such as address and port number, are automatically bound to the socket as the function is called  */
  sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
  /*  Done, closed  */
  close(sock);
1

At this point, it's actually an initiation into network programming, undoing the simplest use of modern UDP (not even full use yet), but it does interact.
UDP is introduced not because it is simple, but because it is simple, and not because it is unimportant, but because it is powerful.
Never underestimate a simple thing, like C

ARP agreement

The easiest way to do this is to find a *nix platform with WireShark or tcpdump. In the former, you can choose to listen to any machine you want and you will soon see the ARP protocol in use because it is used too often.
For the ARP protocol, first of all, if a machine A wants to communicate with the machine B, (assuming that there is no machine B cache in the machine A cache (operating system 1 is updated at a fixed time once),
Then the machine A sends an ARP request to the broadcast address, and if the machine B receives the request, it fills in its own information (IP address, MAC address) in the ARP reply and sends it back.
In the above, the ARP request and ARP reply are information in the form of a message, an implementation product attached to the ARP protocol, and also used for communication between two hosts.
This is when the machine A and the machine B are in the same network, the request message can be sent by the broadcast address of the network segment.
For machine A and machine B in different network segments, to obtain the MAC address through the ARP protocol, we need the help of the router. It can be imagined that the router (can be more than one) is in the middle, and the machine A and machine B are on both sides of these routers (i.e. on different subnets).
Since A and B are not in the same subnet, there is no way to reach them through direct broadcast. However, with the router, it can operate the ARP agent, which is basically to treat the router as a machine, B, and A to send ARP requests to its local router
The router then judges that the ARP request is sent to B, and B happens to be within its jurisdiction, so it writes its hardware address into the ARP reply and sends it back. After that, all the data from A to B are sent by A to the router first and then via the router to B

ICMP agreement

This is an important agreement.
Request reply message and error message focus on error message.
In the application of ICMP, the request reply message can be used to query the information such as the subnet mask of the machine. The request message can be sent to all the hosts in the subnet (including itself, in fact, broadcast), and then receive the reply and get the information
The error message will be mentioned in the future, where science popularization 12 is required.
First for 1 error message are mostly about xxx inaccessible type, such as host unreachable, port inaccessible, and so on, every time there is an error, ICMP message is always the first time back to the client, (it will only appear 1 1 time, otherwise it will cause the network storm), but whether can receive the end, the problem is not sent.
At this point, the socket type has a definite connection; for example, UDP ignores ICMP messages in the unconnected state. Since TCP is always connected to connected, it can capture ICMP messages well.
The ICMP error message always carries a portion of the real data from the error datagram for matching purposes.


Related articles: