Network programming socket (on)

Table of contents

1. Preliminary knowledge

1.1 Port number

1.2 Preliminary understanding of TCP protocol and UDP protocol

1.3 Network byte order

Two, socket programming interface

2.1 Common socket APIs

2.2 sockaddr structure

3. UDP network program

3.1 Server initialization

3.1.1 The server creates a socket

3.1.2 Server Binding

3.1.3 String IP VS Integer IP

3.2 Start and run the server

3.3 Client initialization

3.3.1 The client creates a socket

3.3.2 Client binding problem

3.4 Start and run the client

3.5 Local testing

3.6 INADDR_ANY

3.7 Echo function

3.8 Network Test


1. Preliminary knowledge

1.1 Port number

The essence of socket communication

The data can be sent to the peer host through the IP address and MAC address, but in fact, I want to send the data to a service process on the peer host. In addition, the sender of the data is not the host, but the host on the host. A certain process, such as when using a browser to access, is actually a request initiated by the browser process to the peer service process

Socket communication is essentially communication between two processes, but here is inter-process communication across the network. For example, the actions of visiting Taobao and browsing Douyin actually mean that the Taobao client process and Douyin client process on the mobile phone communicate with the Taobao service process and Douyin service process on the opposite server host

In addition to pipelines, message queues, semaphores, shared memory, etc., inter-process communication methods also include sockets, but the former does not cross the network, while the latter can cross the network or not

The port number

There may be multiple processes that are communicating across the network on the two hosts at the same time, so when the data reaches the peer host, it is necessary to find the corresponding service process on the host through some method, and then hand over the data to the process for processing . And when the process finishes processing the data, it needs to respond to the sender, so the peer host also needs to know which process on the sender sent the data request

The function of the port number is to identify a certain process on a host:

  • The port number is the content of the transport layer protocol
  • The port number is a 2-byte 16-bit integer
  • The port number is used to identify a process, so that the operating system knows which process the current data should be handed over to.
  • A port number can only be occupied by one process

When data is encapsulated at the transport layer, information corresponding to the source port number and destination port number will be added. At this time, the process of sending data can be uniquely identified on the network through the source IP address + source port number, and the process of receiving data can be uniquely identified on the network through the destination IP address + destination port number. interprocess communication

Note: Because the port number belongs to a certain host, the port number can be repeated in two different hosts, but the port number of the process performing network communication on the same host cannot be repeated. In addition, a process can bind multiple port numbers, but a port number cannot be bound by multiple processes at the same time

prot VS PID

The port number (port) can uniquely identify a process on a host, and the process ID (PID) also uniquely identifies a process on a host, so why not directly use PID instead of port in network communication Woolen cloth?

The process ID (PID) is used to identify the uniqueness of all processes in the system, which belongs to the concept of the system; the port number (port) is used to identify the uniqueness of the process that needs to request network data externally, and belongs to the concept of the network

Although PID can be used to identify the uniqueness of the network process, it will make the system part and the network part interleaved, resulting in a high degree of coupling

And a process can bind multiple port numbers, but a process can only correspond to one PID

How to find the corresponding process through port?

The actual bottom layer uses the hash method to establish the mapping relationship between the port number and the process PID or PCB. When the bottom layer gets the port number, it can directly execute the corresponding hash algorithm, and naturally find the process corresponding to the port number.

1.2 Preliminary understanding of TCP protocol and UDP protocol

The network protocol stack runs through the entire architecture and exists in the application layer, operating system layer and driver layer. When using system calls to implement network communication, the protocol layer that has to be faced is the transport layer, and the two most typical protocols of the transport layer are the TCP protocol and the UDP protocol.

TCP protocol

The TCP protocol is called the Transmission Control Protocol (Transmission Control Protocol) , and the TCP protocol is a connection-oriented, reliable, byte-stream-based transport layer communication protocol

The TCP protocol is connection-oriented. If you want to transmit data between two hosts, you must first establish a connection, and the data transmission can only be performed after the connection is successfully established. Secondly, the TCP protocol is a protocol that guarantees reliability. If packet loss or out-of-sequence occurs during data transmission, the TCP protocol has a corresponding solution.

UDP protocol

The UDP protocol is called the User Datagram Protocol (User Datagram Protocol) . The UDP protocol is an unreliable, datagram-oriented transport layer communication protocol that does not need to establish a connection.

There is no need to establish a connection when using the UDP protocol for communication. If two hosts want to transmit data, just send the data to the peer host directly, but it also means that the UDP protocol is unreliable. In the event of packet loss, disorder, etc., the UDP protocol is unaware

Since the UDP protocol is unreliable, why does the UDP protocol exist?

Reliability requires more work. Although the TCP protocol is a reliable transmission protocol, the TCP protocol needs to do more work at the bottom layer, so the bottom layer implementation of the TCP protocol is more complicated.

Although the UDP protocol is an unreliable transmission protocol, the UDP protocol does not need to do too much work at the bottom layer, so the implementation of the bottom layer of the UDP protocol is simpler than the TCP protocol. Although the UDP protocol is unreliable, it can quickly transfer data sent to the other party

Whether to use TCP protocol or UDP protocol when writing network communication code depends entirely on the application scenario of the upper layer. If the application scenario strictly requires the reliability of the data during transmission, the TCP protocol is used at this time; if the application scenario allows a small amount of packet loss during data transmission, then the UDP protocol is preferred, because the UDP protocol is simple and fast enough

Note: Some excellent websites use TCP and UDP protocols at the same time when designing network communication algorithms. When the network is smooth, UDP protocol is used for data transmission, and when the network speed is not good, TCP protocol is used for data transmission. Dynamic Algorithms for tuning background data communications

1.3 Network byte order

Computers have the concept of big and small ends when storing data:

  • Big-endian mode: the high-byte content of the data is stored at the low address of the memory, and the low-byte content of the data is stored at the high address of the memory
  • Little endian mode: the high byte content of the data is stored at the high address of the memory, and the low byte content of the data is stored at the low address of the memory

If the program you write is only run on the local machine, you don’t need to consider the big and small endian issues. The data on the same machine is stored in the same way. Either the big endian storage mode is used, or the small endian storage mode is used. end storage mode.

However, if network communication is involved, the issue of big and small ends must be considered, otherwise the data identified by the peer host may be inconsistent with the data that the sender wants to send

For example, for network communication between two hosts, the sending end is a little-endian machine, and the receiving end is a big-endian machine. After the sending end sends the data in the sending buffer in the order of memory addresses from low to high, when the receiving end obtains data from the network and stores them in the receiving buffer in sequence, it is also stored in the order of memory addresses from low to high, but Little-endian machines and big-endian machines interpret data in memory differently

For the sequence whose memory address is 44332211 from low to high, the sending end recognizes it as 0x11223344 in the way of little endian, and the receiving end recognizes it as 0x44332211 in the way of big endian. The data is different, which is due to the deviation of the big and small ends, which leads to errors in data recognition

How to solve big and small endian difference information?

The TCP/IP protocol stipulates that network data flow adopts big-endian byte order, that is, low address and high byte. Whether it is a big-endian machine or a little-endian machine, data must be sent and received in accordance with the network byte order specified by the TCP/IP protocol.

  • If the sending end is little endian, first convert the data to big endian, and then send it to the network
  • If the sending end is big endian, it can be sent directly
  • If the receiving end is little endian, first convert the received data into little endian and then perform data identification
  • If the receiving end is big endian, data identification can be performed directly

As follows, the sending end is a little-endian machine, and before sending the data, it converts the data into big-endian and then sends it to the network. Since the receiving end is a big-endian machine, the receiving end can directly identify the data after receiving the data. At this time, the data recognized by the receiving end is the same as the data originally intended to be sent by the sending end.

Most of the big and small endian conversion work is done by the operating system, and this operation is a communication detail. There are also some information that need to be processed by the programmer, such as port number and IP address

Why is the network byte order using big endian? instead of little endian?

  • TCP has existed in the Unix era. In the past, Unix machines were all big-endian machines, so the network byte order was big-endian, but later people found that using little-endian can simplify hardware design, so now the mainstream machines are all little-endian machines. , but the protocol is inconvenient to change
  •  Big endian is more in line with the reading and writing habits of modern people

Conversion between network byte order and host byte order

In order to make the network program portable, so that the same C code can run normally after compiling on big-endian and little-endian computers, the following library functions can be called to realize the conversion between network byte order and host byte order

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h in the function name means host, n means network, l means 32-bit long integer, s means 16-bit short integer

  • For example, htonl() means converting a 32-bit long integer from host byte order to network byte order
  • If the host is little-endian, the function converts the parameters to the corresponding big-endian and returns
  • If the host is big-endian, these functions do not perform any conversion and return the arguments unchanged

Two, socket programming interface

2.1 Common socket APIs

Create socket: (TCP/UDP, client+server)

int socket(int domain, int type, int protocol);

Binding port number: (TCP/UDP, server)

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Listening socket: (TCP, server)

int listen(int sockfd, int backlog);

Receive request: (TCP, server)

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

Establish connection: (TCP, client)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2.2 sockaddr structure

The appearance of the sockaddr structure

Sockets not only support inter-process communication across networks, but also local inter-process communication (inter-domain sockets). The port number and IP address that need to be passed during cross-network communication, but not required for local communication, so the socket provides the sockaddr_in structure and the sockaddr_un structure, the sockaddr_in structure is used for cross-network communication, and the sockaddr_un structure is used for local communication

In order to allow the network communication and local communication of the socket to use the same set of function interfaces, the sockaddr structure appeared, which is different from the structure of sockaddr_in and sockaddr_un, but the 16 headers of these three structures bit, this field is called the protocol family

At this time, when passing parameters, it is not necessary to pass in the sockeaddr_in or sockeaddr_un structure, but uniformly pass in a structure such as sockeaddr. When setting parameters, you can set the protocol family field to indicate whether network communication or local communication is required. In these APIs, the 16 bits of the header of the sockeaddr structure are extracted for identification, and then it is determined whether network communication or local communication is required, and then the corresponding operation is performed. At this time, the parameter types of socket network communication and local communication are unified through the general sockaddr structure

Note: In actual network communication, the sockaddr_in structure variable is still defined, but the type of the structure variable address needs to be forced to sockaddr* when passing parameters

  • The address format of IPv4 and IPv6 is defined in netinet/in.h, and the IPv4 address is represented by the sockaddr_in structure, including 16-bit address type, 16-bit port number and 32-bit IP address
  • IPv4 and IPv6 address types are defined as constants AF_INET and AF_INET6 respectively. As long as the first address of the sockaddr structure is obtained, the content of the structure can be determined according to the address type field without knowing what type of sockaddr structure it is
  • The socket API can be represented by the struct sockaddr* type, which needs to be converted into sockaddr_in when used; the advantage of this is the versatility of the program, which can receive IPv4, IPv6, and various types of sockaddr structure pointers of UNIX Domain Socket as parameter

Why not use void* instead of struct sockaddr* type? 

You can change the struct sockaddr* parameter type of these functions to void*. At this time, you can also directly specify the 16 bits of the header to be extracted inside the function for identification, and finally you can judge whether network communication or local communication is required. Why? How about designing a structure like sockaddr?

In fact, when designing this set of network interfaces, the C language did not support void*, so a solution such as sockaddr was designed. And after the C language supports void*, it has not been changed back, because these interfaces are system interfaces, and the system interface is the cornerstone of all upper-layer software interfaces. The system interface cannot be easily changed, otherwise the consequences will be unthinkable. This is why the sockaddr structure is still retained

3. UDP network program

3.1 Server initialization

3.1.1 The server creates a socket

Encapsulate the server into a class. When a server object is defined, the server needs to be initialized immediately, and the first thing to do when initializing the server is to create a socket

socket function

  • domain: The domain (protocol family) where the socket is created, that is, the type of socket created. This parameter is equivalent to the first 16 bits of the struct sockaddr structure. Set to AF_UNIX for local communication, and AF_INET (IPv4) or AF_INET6 (IPv6) for network communication
  • type: The type of service required when creating the socket. The most common service types are SOCK_STREAM and SOCK_DGRAM. If it is UDP-based network communication, it uses SOCK_DGRAM (user datagram service); if it is TCP-based network communication, it uses SOCK_STREAM (streaming socket), which provides streaming services
  • protocol: The protocol class for creating the socket. It can be specified as TCP or UDP, but generally this field can be set to 0, which means the default. At this time, it will automatically deduce which protocol to use in the end according to the first two parameters passed in.

Return value: A file descriptor is returned if the socket is created successfully, and -1 is returned if the creation fails, and the error code will be set at the same time

What type of interface does the socket function belong to?

The network protocol stack is layered. According to the TCP/IP four-layer model, it is the application layer, transport layer, network layer and data link layer from top to bottom. The code written now is called user-level code, that is, the code is written in the application layer, so the actual call is the interface of the lower three layers, and the transport layer and the network layer are completed in the operating system, so socket( ) belongs to the system call interface

What does the underlying socket function do?

The socket function is called by the process, and each process has a PCB (task_struct), a file descriptor table (files_struct) and various open files at the system level. The file descriptor table contains an array fd_array, where the subscripts 0, 1, and 2 in the array correspond to standard input, standard output, and standard error.

When the socket function is called to create a socket, it is actually equivalent to opening a "network file". After opening, a corresponding struct file structure is formed at the kernel level, and the structure is connected to the process corresponding to the The file double-linked list, and fill the first address of the structure into the position with subscript 3 in the fd_array array. At this time, the pointer with subscript 3 in the fd_array array points to the opened "network file", and the last number 3 The file descriptor is returned to the user as the return value of the socket function

Each of the struct file structures contains various information corresponding to the open file, such as file attribute information, operation methods, and file buffers. The attributes corresponding to the file are maintained by the struct inode structure in the kernel, and the operation methods corresponding to the file are actually a bunch of function pointers (such as read* and write*) in the kernel. It is maintained by the struct file_operations structure of. The file buffer usually corresponds to the disk for the ordinary file opened, but for the "network file" opened now, the file buffer corresponds to the network card

For ordinary files, when the user writes data to the file buffer through the file descriptor, and then flushes the data to the disk, the data writing operation is completed. For the "network file" opened by the socket function, when the user writes the data to the file buffer, the operating system will periodically flush the data to the network card, and the network card is responsible for sending the data, and the data is finally sent to the network

Code

When initializing the server to create a socket, it is to call the socket function to create a socket. The protocol family that needs to be filled in when creating a socket is AF_INET, because the network communication is to be performed, and the required service type is SOCK_DGARM, because now The UDP server written is datagram-oriented, and the third parameter can be set to 0

//UdpServer.h
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>

using std::cout;
using std::cerr;
using std::endl;

class UdpServer
{
public:
    bool InitServer();
    ~UdpServer();
private:
    int _socket_fd;
};
//UdpServer.cc
#include "UdpServer.h"

bool UdpServer::InitServer() {
    _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        return false;
    }
    cout << "socket create success" << endl;
    return true;
}

UdpServer::~UdpServer() {
    if(_socket_fd > 0) close(_socket_fd);
}

When the server is destroyed, the file corresponding to _socket_fd can be closed, but in fact, it is not necessary to perform this operation, because the general server will not stop after running

#include "UdpServer.h"

int main() 
{
    UdpServer* server = new UdpServer;
    server->InitServer();
    return 0;
}

After running the program, the socket is successfully created, and the obtained file descriptor is 3, because 0, 1, and 2 are occupied by the standard input stream, standard output stream, and standard error stream by default. At this time, the smallest and unused file Descriptor is 3

3.1.2 Server Binding

The socket has been successfully created, but as a server, if the socket is only created, it is only a file opened at the system level, and the operating system will not know that it is going to write data to the disk in the future It is still flashing to the network card, and the file has not been associated with the network at this time. So the second thing to do to initialize the server is to bind

bind function

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: The file descriptor of the bound file. That is, the file descriptor we got when we created the socket
  • addr: network-related attribute information, including protocol family, IP address, port number, etc.
  • addrlen: the length of the incoming addr structure

Return value: 0 is returned if the binding is successful, -1 is returned if the binding fails, and the error code will be set at the same time

struct sockaddr_in structure

When binding, you need to fill the network-related attribute information into a structure, and then pass the structure as the second parameter of the bind function, which is the struct sockaddr_in structure

The definition of struct sockaddr_in can be found in /usr/include/linux/in.h

  • sin_family: Indicates the protocol family
  • sin_port: Indicates the port number, which is a 16-bit integer
  • sin_addr: Indicates the IP address, which is a 32-bit integer

The remaining fields are generally not processed, of course, they can also be initialized

The type of sin_addr is struct in_addr. In fact, there is only one member in the structure, which is a 32-bit integer. The IP address is actually stored in this integer.

How to understand binding?

When binding, you need to tell the corresponding network file the IP address and port number. At this time, you can change the pointing of the file operation function in the network file, change the corresponding operation function to the operation method of the corresponding network card, and read data at this time. The operation object corresponding to writing data is the network card, so the binding is actually to associate the file with the network

Code

Since the IP address and port number are required for binding, the IP address and port number need to be introduced into the server class, and the corresponding IP address and port number need to be passed in when creating the server object. IP address and port number to initialize

class UdpServer
{
public:
    UdpServer(uint16_t port,string ip):_socket_fd(-1),_port(port),_ip(ip) {}
    bool InitServer();
    ~UdpServer();
private:
    int _socket_fd;
    uint16_t _port;
    string _ip;
};

After the socket is created, it needs to be bound, but before binding, I need to define a struct sockaddr_in structure variable, and fill the corresponding network attribute information into the structure. Since there are some optional fields in the structure, it is best to clear the contents of the structure variable before filling, and then fill the protocol family, port number, IP address and other information into the structure variable

Before sending to the network, the port number needs to be set as a network sequence. Since the port number is 16 bits, it is necessary to use the htons() function to convert the port number into a network sequence. In addition, since the integer IP is transmitted in the network, it is necessary to call the inet_addr() function to convert the string IP into an integer IP (at the same time into a network sequence), and then set the converted integer IP

After the network attribute information is filled, since the bind function provides a general parameter type, it is also necessary to convert the struct sockaddr_in* to the struct sockaddr* type before passing in the structure address

bool UdpServer::InitServer() {
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        return false;
    }
    cout << "socket create success , fd:" << _socket_fd << endl;

    //填充网络通信相关信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = inet_addr(_ip.c_str());
    //绑定
    if(bind(_socket_fd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        cerr << "bind fail" << endl;
        return false;
    }
    cout << "bind success" << endl;

    return true;
}

3.1.3 String IP VS Integer IP

When transmitting data over the network, land is very expensive. If the IP address is transmitted directly in the form of a string-based dotted decimal IP during network transmission, then an IP address needs 15 bytes at this time, but it does not actually cost so many bytes

The IP address can actually be divided into four areas, and the value of each area is 0~255, and the number in this range only needs to be represented by 8 bits, so only 32 bits are needed to represent a IP address. Each byte of the 32-bit integer corresponds to a certain area in the IP address, and this representation of the IP address is called an integer IP

The scheme of using integer IP to represent an IP address only needs 4 bytes, and it can also represent the same meaning. Therefore, in the network communication, the string IP is not used but the integer IP is used, which reduces the transmission of data in network communication.

Convert between string IP and integer IP

There are many conversion methods. For example, a bit segment A can be defined. There are four members in the bit segment A, and the size of each member is 8 bits. 32 bits

Then define a consortium IP, which has two members, one of which is a 32-bit integer, which represents the integer IP, and the other is a member of bit segment A type, which represents the string IP

Since the space of the union is shared by members, the way to set and read IP is as follows:

  • When you want to set the IP in the form of an integer IP, you can directly assign it to the first member of the union
  • When you want to set the IP in the form of a string IP, first divide the string into four corresponding parts, and then convert each part into a corresponding binary sequence and set them to p1, p2, p3 and P4 is enough
  • When you want to get the integer IP, you can directly read the first member of the union
  • When you want to take out the string IP, get p1, p2, p3, and p4 in the second member of the consortium in turn, and then convert each part into a string and splice them together

Note: Bit segments and enumerations are actually used inside the operating system to complete the conversion between string IP and integer IP

inet_addr function

Convert the string IP to an integer IP, and convert it to a network sequence

in_addr_t inet_addr(const char *cp);

inet_ntoa function

Convert integer IP to string IP and convert to host sequence

char *inet_ntoa(struct in_addr in);

The parameter type passed in to the inet_ntoa function is in_addr, so it is not necessary to select the 32-bit member in the in_addr structure to pass in when passing the parameter, just directly pass in the in_addr structure

3.2 Start and run the server

The initialization of the UDP server only needs to create a socket and bind it. When the server is initialized, the server can be started.

The server is actually providing some kind of service over and over again. The reason why the server is called a server is because the server will never exit after running, so what the server actually executes is an infinite loop code. Since the UDP server is not connection-oriented, as long as the UDP server is started, the data sent by the client can be directly read

recvfrom function

read data

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd: The file descriptor corresponding to the operation. Indicates reading data from the file indexed by this file descriptor
  • buf: the storage location of the read data
  • len: the number of bytes of data expected to be read
  • flags: the way to read. Generally set to 0, which means blocking read
  • src_addr: attribute information related to the peer network, including protocol family, IP address, port number, etc. (output parameter, but cannot be set to nullptr or NULL)
  • addrlen: The length of the src_addr structure that is expected to be read is passed in when calling, and the length of the actually read src_addr structure is represented when it is returned (input and output parameters)

Return value: If the read is successful, the number of bytes actually read will be returned; if the read fails, -1 will be returned, and the error code will be set at the same time

  • Since UDP is not connection-oriented, in addition to obtaining data, it is also necessary to obtain attribute information related to the peer network, including IP address and port number, etc.
  • When calling recvfrom to read data, addrlen must be set to the size corresponding to the structure to be read
  • Since the parameters provided by the recvfrom function are also of the struct sockaddr* type, the struct sockaddr_in* type needs to be forcibly transferred when passing in the structure address

Provides an interface to start the server

The server reads the client data through the recvfrom function. The read data can be regarded as a string first, and the last position of the read data is set to '\0'. At this time, the read data can be read The data is output, and the obtained IP address and port number of the client can also be output together.

The obtained port number of the client is a network sequence at this time, and it needs to call the ntohs function to convert it into a host sequence and then print it out. The obtained IP address of the client is an integer IP, which needs to be converted into a string IP (converted into a host sequence) by calling the inet_ntoa function and then output

void UdpServer::Start()
{
    char buffer[SIZE];
    while(true) 
    {
        struct sockaddr_in ping;
        socklen_t length =  sizeof(ping);
        ssize_t size = recvfrom(_socket_fd, buffer, SIZE - 1, 0, (struct sockaddr*)&ping, &length);
        if(size > 0) {
            buffer[size] = '\0';
            uint16_t port = ntohs(ping.sin_port);
            string ip = inet_ntoa(ping.sin_addr);
            cout << "[" << ip << ":" << port << "]#" << buffer << endl;
        }
        else {
            cerr << "recvfrom fail" << endl;
        }
    } 
}

Note: If calling the recvfrom function fails to read data, you can print a prompt message, but do not let the server exit. The server cannot exit because it fails to read data from a certain client.

Introduce command line parameters

Since the IP address and port number need to be passed in when constructing the server, command line parameters can be introduced. At this time, when running the server, just follow the corresponding IP address and port number.

Currently using the IP address 127.0.0.1. The IP address of 127.0.0.1 is equivalent to localhost to indicate the local host, which is called local loopback. The data only flows in the local protocol stack and does not pass through the network. First test whether the normal communication can be performed locally, and then perform the network communication test

int main(int argc, char* argv[]) 
{
    if(argc < 3) {
        cerr << "Usage " << argv[0] << " ip port" << endl;
        return 1;
    }
    UdpServer* server = new UdpServer(string(argv[1]),atoi(argv[2]));
    server->InitServer();
    server->Start();
    return 0;
}

The agrv array stores character pointers, and the port number is an integer. You need to use the atoi function to convert the string into an integer. Then you can use this IP address and port number to construct the server. After the server is constructed and initialized, you can call the Start function to start the server.

The client code has not been written yet, you can use the netstat command to view the current network status, here you can choose the nlup option

  • -n: use the IP address directly without going through the domain name server
  • -l: Display the Socket of the server being monitored
  • -t: Display the connection status of the TCP transport protocol
  • -u: Display the connection status of the UDP transport protocol
  • -p: Display the program identification code and program name that is using Socket

Remove the n option, and the location that originally displayed the IP address becomes the corresponding domain name server

Proto indicates the type of protocol, Recv-Q indicates the network receiving queue, Send-Q indicates the network sending queue, Local Address indicates the local address, Foreign Address indicates the external address, State indicates the current state, PID indicates the process ID of the process, Program name Indicates the program name of the process

Among them, Foreign Address is written as 0.0.0.0 :*, which means that any program with any IP address and any port number can access the current process

3.3 Client initialization

3.3.1 The client creates a socket

Encapsulate the client into a class. When a client object is defined, it also needs to be initialized, and the client also needs to create a socket when it is initialized. Afterwards, the client sends data or receives data to this socket. word to operate

The protocol family selected by the client when creating a socket is AF_INET, and the required service type is SOCK_DGARM. When the client is destroyed, it can choose to close the corresponding socket. Unlike the server, the client only needs to create a socket during initialization without binding

class UdpClient
{
public:
    UdpClient(string server_ip,uint16_t server_port):_server_ip(server_ip),_server_port(server_port) {}
    bool InitClient();
    ~UdpClient() {}
private:
    int _socket_fd;
    string _server_ip;
    uint16_t _server_port;
};


bool UdpClient::InitClient() 
{
	_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (_socket_fd < 0){
		std::cerr << "socket create error" << std::endl;
		return false;
	}
	return true;
}

UdpClient::~UdpClient() { if(_socket_fd < 0) close(_socket_fd); }

3.3.2 Client binding problem

Because it is network communication, both parties need to find each other, so both the server and the client need to have their own IP addresses and port numbers, but the server needs to bind the port number, but the client does not.

Because the server is to provide services to others, the server must let others know its IP address and port number. The IP address generally corresponds to the domain name, and the port number is generally not indicated, so the port number of the server must be a Well-known port number, and it cannot be easily changed after selection, otherwise the client cannot know the port number of the server, which is why the server needs to bind. Only after binding, the port number really belongs to itself, because a A port can only be bound by one process, and the server binds a port to monopolize the port

Although the client also needs a port number when communicating, the client is generally not bound. When the client accesses the server, the port number only needs to be unique and does not need to be strongly related to a specific client process.

If the client is bound to a certain port number, then this port number can only be used by this client in the future, even if this client is not started, this port number cannot be assigned to others, and if this port number is used by others , then the client cannot be started. Therefore, the port of the client only needs to be unique. Therefore, the client port can be dynamically set, and the port number of the client does not need to be set by the programmer. When calling an interface like sendto(), the operating system will automatically obtain a unique port number for the current client.

The port number used by the client may change each time it is started. At this time, as long as the port number is not exhausted, the client can be started normally.

3.4 Start and run the client

After the client is initialized, the client can be run. Since the client and the server are functionally complementary, since the server is reading the data sent by the client, the client should send the data to the server. data

sendto function

send data

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd: The file descriptor corresponding to the operation. Indicates writing data to the file indexed by this file descriptor
  • buf: the storage location of the data to be written
  • len: the number of bytes expected to write data
  • flags: the way to write. Generally set to 0, which means blocking write
  • dest_addr: attribute information related to the peer network, including protocol family, IP address, port number, etc.
  • addrlen: the length of the incoming dest_addr structure

Return value: the number of bytes actually written is returned if the write is successful, and -1 is returned if the write fails, and the error code will be set at the same time

  • Since UDP is not connection-oriented, in addition to passing in the data to be sent, it is also necessary to specify information related to the peer network, including IP address and port number, etc.
  • Since the parameter provided by sendto() is of type struct sockaddr*, the type of struct sockaddr* needs to be forcibly converted when passing the parameter

Provides an interface to start the client

If the client wants to send data to the server, the client can obtain user input and continuously send the data input by the user to the server

The port number of the server stored in the client is the host sequence at this time, it needs to call the htons() function to convert it into a network sequence and then set it into the struct sockaddr_in structure. The IP address of the server stored in the client is a string IP, which needs to be converted into an integer IP (and converted into a network sequence) by calling the inet_addr() function and then set into the struct sockaddr_in structure

void UdpClient::Start()
{
    string message;
    struct sockaddr_in receive;
    memset(&receive, 0, sizeof(receive));
    receive.sin_port = htons(_server_port);
    receive.sin_family = AF_INET;
    receive.sin_addr.s_addr = inet_addr(_server_ip.c_str());

    while(true)
    {
        cout << "please Enter#";
        getline(cin, message);
        sendto(_socket_fd, message.c_str(), strlen(message.c_str()), 0, (struct sockaddr*)&receive, sizeof(receive));
    }
}

Introduce command line parameters

Introduce the command line parameters, and directly follow the IP address and port number of the corresponding server when running the client

int main(int argc, char* argv[])
{
    if(argc < 3) {
        cerr << "Usage " << argv[0] << " ip port " << endl;
        return 1;
    }
    string serve_ip = argv[1];
    uint16_t serve_port = atoi(argv[2]);
    UdpClient* client = new UdpClient(serve_ip, serve_port);
    client->InitClient();
    client->Start();
    return 0;
}

3.5 Local testing

The codes of the server and client have been written, and the local test can be performed first. At this time, the server is not bound to the external network, but is bound to the local loopback. Now specify the port number as 8080 when running the server, and then run the client. At this time, the IP address of the server to be accessed by the client is the local loopback, and the port number of the server is 8081.

Use the netstat command to view network information, you can see that the port of the server is 8081, the port of the client is 36577, and the client has been dynamically bound successfully

3.6 INADDR_ANY

Perform a network test and directly bind the server to the public IP, then the server can be accessed by the external network

Change the local loopback set on the server to the public IP of the blogger. At this time, when the server is re-run, it will be found that the server binding fails

Since the IP address of the cloud server is provided by the corresponding cloud vendor, this IP address is not necessarily the real public network IP. This IP address cannot be directly bound. If you need to allow external network access, you need to bind 0. An INADDR_ANY (macro value) provided by the system, the corresponding value is 0

If you need to allow access from the external network, you should bind INADDR_ANY when you bind, so that the server can be accessed by the external network

Benefits of binding INADDR_ANY

When the bandwidth of a server is large enough, the ability of a machine to receive data constrains the IO efficiency of this machine, so a server may have multiple network cards installed at the bottom layer, and this server may have multiple IPs address, but there is only one service with port number 8081 on a server. When this server is receiving data, multiple network cards have actually received the data at the bottom layer. Maybe these data also want to access the service with port number 8081

At this time, if the server specifies a bound IP address when binding, then the server can only receive data from the network card corresponding to the bound IP when receiving data. And if the server is bound to INADDR_ANY, then as long as the data is sent to the service with port number 8081, the system will be able to hand over the data to the server from bottom to top

Therefore, the scheme of binding INADDR_ANY on the server side is strongly recommended, and all servers also adopt this scheme during operation.

If you want to access your server from the external network, but also want to point to a certain IP address, then you cannot use the cloud server. At this time, you can choose to use a virtual machine or a custom-installed Linux operating system. The IP address is Supports custom binding, but the cloud server does not support it

change code

bool UdpServer::InitServer() {
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        return false;
    }
    cout << "socket create success , fd:" << _socket_fd << endl;

    //填充网络通信相关信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY;
    //绑定
    if(bind(_socket_fd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        cerr << "bind fail" << endl;
        return false;
    }
    cout << "bind success" << endl;

    return true;
}

At this time, when the server is recompiled and running, the binding will not fail, and at this time, when you use the netstat command to view it, you will find that the local IP address of the server becomes 0.0.0.0, which means that the UDP server can be read locally. Get data from any network card

3.7 Echo function

Because during the network test, when the client sends data to the server, the server will print the data received from the client, so the server can see the phenomenon. But the client has been sending data to the server, and the client cannot see whether the server has received the data it sent

Server code writing

This server can be changed to an echo server. When the server receives the data sent by the client, in addition to printing on the server, the server can call the sendto function to resend the received data to the corresponding client

The server needs to pass in the client's network attribute information when calling the sendto function, but the server knows the client's network attribute information, because the server has already obtained the client's network attribute information through the recvfrom function before

void UdpServer::Start()
{
    char buffer[SIZE];
    while(true) 
    {
        struct sockaddr_in ping;
        socklen_t length =  sizeof(ping);
        ssize_t size = recvfrom(_socket_fd, buffer, SIZE - 1, 0, (struct sockaddr*)&ping, &length);
        if(size > 0) {
            buffer[size] = '\0';
            uint16_t port = ntohs(ping.sin_port);
            string ip = inet_ntoa(ping.sin_addr);
            cout << "[" << ip << ":" << port << "]#" << buffer << endl;
        }
        else {
            cerr << "recvfrom fail" << endl;
        }

        string echo_message = "server echo:";
		echo_message += buffer;
		sendto(_socket_fd, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr*)&ping, length);
    } 
}

Client code writing

After the client sends the data to the server, since the server will resend the data to the client, the client needs to call recvfrom to read the response data sent by the server, and the client receives After receiving the response data from the server, just print out the data intact.

At this time, the data sent by the client to the server will not only be printed and displayed on the server, but the server will also resend the data to the client. At this time, the client will also receive the response data and then print the data

void UdpClient::Start()
{
    string message;
    struct sockaddr_in receive;
    memset(&receive, 0, sizeof(receive));
    receive.sin_port = htons(_server_port);
    receive.sin_family = AF_INET;
    receive.sin_addr.s_addr = inet_addr(_server_ip.c_str());

    while(true)
    {
        cout << "please Enter#";
        getline(cin, message);
        sendto(_socket_fd, message.c_str(), strlen(message.c_str()), 0, (struct sockaddr*)&receive, sizeof(receive));

        char buffer[SIZE];
		struct sockaddr_in tmp;
		socklen_t length = sizeof(tmp);
		ssize_t size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &length);
		if (size > 0) {
			buffer[size] = '\0';
			cout << buffer << endl;
		}
    }
}

 

3.8 Network Test

At this point, you can use the sz command to download the client executable program to the local machine, and then send the program to your friends

After your friend receives the executable program of the client, he can use the rz command or drag and drop to upload the executable program to his cloud server, and then use the chmod command to add executable permissions to the file

Start the server first, and then your friend can access your server by running the client with your IP address and port number as command line parameters

Guess you like

Origin blog.csdn.net/GG_Bruse/article/details/129760960