Linux - Ubuntu web server development (full)

Linux - Ubuntu web server development

Linux - Ubuntu server development

The interface between the server and the client is through Socket communication

Socket communication has three elements:

  1. Target address of communication (ip address of server)
  2. Port number used (different protocols specify different ports, for example, port 25 corresponds to stmp protocol and port 80 corresponds to http protocol)
  3. Transport layer protocol used( TCPUDP , these two are written in QT, but you can refer to communication logic)
Server development
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>

#define SERVER_PORT 666

int main(void)
{
    // Create mailbox, AF_INET is protocol family IPV4, SOCK_STREAM is a TCP connection type
    int sock;       // mailbox
    struct sockaddr_in server_addr;	// label
    sock = socket(AF_INET, SOCK_STREAM, 0);

    // Clear the label and write the address and port number
    bzero(&server_addr, sizeof(server_addr));

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);// Listen to all local IP addresses

    server_addr.sin_port = htons(SERVER_PORT);      // Binding port number

    // Attach the label to the mailbox
    bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));

    // Open the mailbox on the network, and 128 is the number of connections that can be accepted at the same time
    listen(sock, 128);

    // Waiting for letters
    printf("Waiting for client connection...\n");

    int done = 1;
    while(done)
    {
        // Receive the letter and get the client information
        struct sockaddr_in client;	// letter
        int client_sock;
        char client_ip[64];

        socklen_t client_addr_len;
        client_addr_len = sizeof(client);
        client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);	// Accept connection

        // Test information, examples, read, write usage
        // Print client IP address and port number
        printf("client ip: %s\tport: %d\n",
               inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)),
               ntohs(client.sin_port));

        // Read data sent by client
        char buf[256];
        int len;
        len = read(client_sock, buf, sizeof(buf)-1);

        buf[len] = '\0';
        printf("recive: %s\n", buf);

        // Returns the data sent by the client
        len = write(client_sock, buf, len);
        close(client_sock);
    }

    return 0;
}

Q: What are htonl, htons and ntohs?
A: It is a type conversion function, mainly the conversion between host byte (h) and network byte (n) (described in detail later)

Test connection

Win + R runs cmd and uses the command of telnet [IP] [PORT] to connect to the server. The server ip address can be viewed under link/ether using ip addr

If the telnet command is invalid, start the service:

  1. Windos 10 (same as Win7)
    window settings - > find settings "control panel" - > Programs - > enable or disable Windows functions under programs and functions - > check "Telnet client"

Example: telnet 192.168.220.128 666 the ip address is self configured, and the port is set in the server program

After connection, if * * "client ip: [IP] port: [PORT]" * * is displayed in the program, it is successful

Use the Telnet client, * * ctrl +] * * to start the command mode, and enter sen hello!, It is successful to see that the server has feedback back to the test information

Linux - Ubuntu client development

Client development
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_PORT 666
#define SERVER_IP "127.0.0.1"

int main(int argc, char *argv[])
{
    // Get parameters (content to be sent to the server)
    int sockfd; // mailbox
    char *message;
    struct sockaddr_in servaddr; // Label, envelope
    int n;
    char buf[64];

    if(argc != 2)   // Program parameter error
    {
        fputs("usage: ./echo_client message\n", stderr);
        exit(1);
    }

    message = argv[1];
    //printf("message: %s\n", message);

    // New Mailbox 
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // Clear the label and write the address and port number
    memset(&servaddr, '\0', sizeof(struct sockaddr_in));

    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr); // The IP address to be sent is connected to the local server, so 127.0.0.1 is used

    servaddr.sin_port = htons(SERVER_PORT); // Send to port number

    // Connect server
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    // send content
    write(sockfd, message, strlen(message));

    // Read server content
    n = read(sockfd, buf, sizeof(buf)-1);

    if(n > 0)
    {
        buf[n] = '\0';
        printf("receivs: %s\n", buf);
    }
    else
    {
        perror("error!\n");
    }
    close(sockfd);

    return 0;
}

Test connection

Opening the program requires text with parameters, such as/ a.out "This a message text!"
Usage: start the server, wait for connection - > start the client program, with parameters

supplement
  1. If you report an error when writing your own code, you can use: set nu in vim command mode to display the number of text lines.
  2. After the server is turned on, the input and output of the client connection are normal. If the server does not respond, it may be because the server may have legacy background threads running. You can use ps -ef | grep a.out(a.out is the file name) to check whether there are legacy threads in the background, and then use kill a.out (a.out is the program name) to end these legacy processes

Socket socket

Socket means "socket" in Chinese. In Linux environment, it is used to represent the special file type of network communication between processes x. It is essentially a pseudo file formed by the kernel with the help of buffer.

Since it is a file, of course, we can use the file descriptor to refer to the socket. The purpose of encapsulating it into files in Linux system is to unify the interface and make the operation of reading and writing sockets and reading and writing files consistent. The difference is that files are mainly used for reading and writing local persistent data, while sockets are mostly used for data transmission between network processes.

In TCP/IP protocol, "IP address + TCP or UDP port number" uniquely identifies a process in network communication. "IP address + port number" corresponds to a socket. The two processes that want to establish a connection each have a socket to identify, so the socket pair composed of these two sockets uniquely identifies a connection. Therefore, socket can be used to describe the one-to-one relationship of network connection.

flow chart:

Add: the TCP mentioned earlier is similar to * * "call", which is connection oriented; UDP is similar to "writing a letter" * *.

Network byte order

In the computer world, there are two byte orders:

Big endian byte order - low address high byte, high address low byte
Small end byte order - low address low byte, high address high byte

1001 = 0x03 e9: indicates that the decimal number 1001 needs to be stored in two bytes in the computer, 03 and e9 (hexadecimal) respectively

666. And 667: Address

Multi byte data in memory can be divided into large end and small end relative to memory address, and multi byte data in disk file can also be divided into large end and small end relative to offset address in file. Network data flow can also be divided into large end and small end, so how to define the address of network data flow? The sending host usually sends the data in the sending buffer in the order of memory address from low to high, and the receiving host saves the bytes received from the network in the receiving buffer in the order of memory address from low to high. Therefore, the address of the network data stream should be specified as follows: the data sent first is the low address, and the data sent later is the high address.

TCP/IP protocol stipulates that network data flow shall adopt large end byte order, that is, low address and high byte.

For example, the port number is 1001 (0x3e9), which is saved by two bytes. If the large end byte order is adopted, the low address is 0x03 and the high address is 0xe9, that is, 0x03 is sent first and then 0xe9. These 16 bits should also be 0x03 in the low address and 0xe9 in the buffer of the sending host. However, if the sending host is in small endian byte order, these 16 bits are interpreted as 0xe903 instead of 1001. Therefore, the sending host needs to convert the byte order before filling 1001 into the sending buffer. Similarly, if the receiving host is in small end byte order, the 16 bit source port number also needs to be converted in byte order. If the host is in large byte order, no conversion is required for sending and receiving. Similarly, network byte order and host byte order should also be considered for 32-bit IP addresses.

In order to make the network program portable and make the same C code run normally after being compiled on large-end and small-end computers, the following library functions can be called to convert the 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 represents host, n represents network, l represents 32-bit long integer, and s represents 16 bit short integer.

If the host is in small endian byte order, these functions convert the parameters to large and small endian and then return them. If the host is in large endian byte order, these functions do not convert and return the parameters intact.

Supplement: IP uses long integers and PORT uses short integers

sockaddr data structure

Many network programming functions were born earlier than IPv4 protocol. At that time, sockaddr structure was used. For forward compatibility, sockaddr now degenerates into the role of (void *), passing an address to the function. As for this function, it is sockaddr_in or other, it is determined by the address family, and then the internal type of the function is forcibly converted to the required address type.

sockaddr data structure:

struct sockaddr {

    sa_family_t sa_family; 		/* address family, AF_xxx */
    char sa_data[14];			/* 14 bytes of protocol address */
};

struct sockaddr_in {

    sa_family_t	sin_family;		/* address family: AF_INET */
    in_port_t	sin_port;		/* port in network byte order */
    struct in_addr sin_addr;	/* internet address */
 };

/* Internet address. */
struct in_addr {

    uint32_t    s_addr; 		/* address in network byte order */
};

The IPv4 address format is defined in netinet / in H, the IPv4 address uses sockaddr_in structure, including 16 bit port number and 32-bit IP address, but the implementation of sock API was earlier than ANSI C standardization, and there was no void * type at that time. Therefore, these parameters such as bind and accept functions are represented by struct sockaddr * type. Before passing parameters, forced type conversion is required, for example:

struct sockaddr_in servaddr;

bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));	/*initialize servaddr */
IP address translation function

#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

af value can be AF_INET and AF_INET6, which corresponds to ipv4 and ipv6

Where inet_pton and inet_ntop can not only convert IPv4 in_addr, which can also convert IPv6 in6_addr.

Therefore, the function interface is void *addrptr.

Example:

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

int main(void)
{
    char ip[]="2.3.4.5";	// IPV4
    char server_ip[64];
    struct sockaddr_in server_addr;

    // IPV4 host byte order - > network byte order 	 inet_pton
    inet_pton(AF_INET, ip, &server_addr.sin_addr.s_addr);
    printf("s_addr : %x\n", server_addr.sin_addr.s_addr);
    
    // Network byte order - > host string IP 	 ntohl
    printf("s_addr from net to host: %x\n", ntohl(server_addr.sin_addr.s_addr));

    // Network byte order - > IPv4 host byte order 	 inet_ntop
    inet_ntop(AF_INET, &server_addr.sin_addr.s_addr, server_ip, 64);
    printf("server ip : %s\n", server_ip);
    
    // Macro representing all IP addresses
    printf("INADDR_ANY: %d\n", INADDR_ANY);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    inet_ntop(AF_INET, &server_addr.sin_addr.s_addr, server_ip, 64);
    printf("INADDR_ANY ip : %s\n", server_ip);
    return 0;
}

Output:

s_addr : 5040302
s_addr from net to host: 2030405
server ip : 2.3.4.5
INADDR_ANY: 0
INADDR_ANY ip : 0.0.0.0

Socket programming function

socket function
#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

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

domain:
AF_INET, which is most of the protocols used to generate socket s, uses TCP or UDP for transmission and IPv4 addresses
AF_INET6 is similar to the above, but it uses IPv6 address
AF_UNIX local protocol, which is used on Unix and Linux systems, is generally used when the client and server are on the same computer

type:
SOCK_STREAM is a sequential, reliable and data complete connection based on byte stream. This is the most used socket type. This socket uses TCP for transmission.
SOCK_DGRAM is a connectionless, fixed length transport call. The protocol is unreliable and uses UDP for its connection.
SOCK_SEQPACKET this protocol is a two-wire and reliable connection, which sends fixed length data packets for transmission. The package must be completely accepted before it can be read.
SOCK_RAW socket type provides single network access. This socket type uses ICMP public protocol. (ping and traceroute use this protocol)
SOCK_RDM is rarely used and is not implemented on most operating systems. It is provided to the data link layer and does not guarantee the order of data packets

protocol:
Passing 0 means using the default protocol.

Return value:
Success: returns the file descriptor pointing to the newly created socket
Failure: Return - 1, set errno

socket() opens a network communication port. If it succeeds, it returns a file descriptor like open(). The application can send and receive data on the network with read/write like reading and writing files. If the socket() call is wrong, it returns - 1. For IPv4, the domain parameter is specified as AF_INET. For TCP protocol, the type parameter is specified as SOCK_STREAM, which represents a stream oriented transport protocol. If it is a UDP protocol, the type parameter is specified as SOCK_DGRAM, which represents datagram oriented transmission protocol. The introduction of the protocol parameter is omitted. You can specify 0.

bind function
#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

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

sockfd:
socket file descriptor

addr:
Construct the IP address plus the end address

addrlen:
sizeof(addr) length

Return value:
0 returned successfully
Failure returns - 1, setting errno

The network address and port number monitored by the server program are usually fixed. After the client program knows the address and port number of the server program, it can initiate a connection to the server. Therefore, the server needs to call bind to bind a fixed network address and port number.

The function of bind() is to bind the parameters sockfd and addr together, so that sockfd, the file descriptor used for network communication, can listen to the address and port number described by addr. As mentioned earlier, struct sockaddr * is a general pointer type. The addr parameter can actually accept sockaddr structures of various protocols, and their lengths are different. Therefore, the third parameter addrlen is required to specify the length of the structure. For example:

struct sockaddr_in servaddr;

bzero(&servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

servaddr.sin_port = htons(6666);

//First clear the entire structure, and then set the address type to AF_INET, the network address is INADDR_ANY,
//This macro represents any local IP address, because the server may have multiple network cards, and each network card may bind multiple IP addresses,
//In this way, you can listen on all IP addresses. You can't determine which IP address to use until you establish a connection with a client. The port number is 6666.
listen function
#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd:
socket file descriptor

backlog:
In Linux system, it refers to the length of queue waiting to establish handshake for 3 times

View the system default backlo:: cat /proc/sys/net/ipv4/tcp_max_syn_backlog

Change the backlog size of the system limit: VIM / etc / sysctl conf

Finally add:

net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 1024

Save and execute: sysctl -p

A typical server program can serve multiple clients at the same time. When a client initiates a connection, the server calls accept() to return and accept the connection. If a large number of clients initiate a connection and the server has no time to process it, the client that has not accepted is in the connection waiting state, and listen() declares that sockfd is in the listening state, A maximum of backlog clients are allowed to be connected. If more connection requests are received, they will be ignored. Listen() returns 0 for success and - 1 for failure.

accept function
#include <sys/types.h> 		/* See NOTES */

#include <sys/socket.h>

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

sockdf:
socket file descriptor

addr:
Outgoing parameter, which returns the address information of the linked client, including IP address and port number

addrlen:
Pass in the outgoing parameter (value result), the size of the incoming sizeof(addr), and the size of the actual received address structure when the function returns

Return value:
A new socket file descriptor is returned successfully for communication with the client. If it fails, it returns - 1 and sets errno

Three handshake processes:

After three handshakes, the server calls accept() to accept the connection. If there is no connection request from the client when the server calls accept(), it will block and wait until a client connects. Addr is an outgoing parameter. When accept() returns, the address and port number of the outgoing client. The addrlen parameter is an incoming and outgoing parameter (value result argument). The incoming parameter is the length of the buffer addr provided by the caller to avoid buffer overflow. The outgoing parameter is the actual length of the client address structure (the buffer provided by the caller may not be full). If you pass NULL to the addr parameter, it means that you don't care about the address of the client.

Our server program structure is as follows:

while (1) 
{
	cliaddr_len = sizeof(cliaddr);

	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);

	n = read(connfd, buf, MAXLINE);

	// to do...

	close(connfd);
}

The whole is a while loop, which processes one client connection at a time. Because cliaddr_len is an incoming and outgoing parameter. The initial value should be reset before each call to accept(). The parameter listenfd of accept() is the previous listening file descriptor, and the return value of accept() is another file descriptor connfd. After that, it communicates with the client through this connfd. Finally, close connfd and disconnect without closing listenfd. Return to the beginning of the loop again. Listenfd is still used as the parameter of accept. Accept() returns a file descriptor successfully and - 1 in case of error.

connect function
#include <sys/types.h> 					/* See NOTES */

#include <sys/socket.h>

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

sockdf:
socket file descriptor

addr:
Pass in parameters to specify server-side address information, including IP address and port number

addrlen:
Incoming parameter, incoming sizeof(addr) size

Return value:
0 returned successfully
Failure returns - 1, setting errno

The client needs to call connect() to connect to the server. The parameter forms of connect and bind are the same. The difference is that the parameter of bind is its own address, while the parameter of connect is the address of the other party. Connect() returns 0 successfully and - 1 in error.

Error handling function

We know that the system function call cannot guarantee success every time, and error handling must be carried out. On the one hand, it can ensure the normal program logic, on the other hand, it can quickly get the fault information.

#include <errno.h>

#include <string.h>

char *strerror(int errnum);   /* See NOTES */

errnum:
The value of the error number of the passed in parameter is generally the value of errno

Return value:
Cause of error

#include <stdio.h>

#include <errno.h>

void perror(const char *s);   /* See NOTES */

s:
Pass in parameters, custom description

Return value:
nothing

Output the error reason to stderr

-1. Set errno

The client needs to call connect() to connect to the server. The parameter forms of connect and bind are the same. The difference is that the parameter of bind is its own address, while the parameter of connect is the address of the other party. Connect() returns 0 successfully and - 1 in error.

Error handling function

We know that the system function call cannot guarantee success every time, and error handling must be carried out. On the one hand, it can ensure the normal program logic, on the other hand, it can quickly get the fault information.

#include <errno.h>

#include <string.h>

char *strerror(int errnum);   /* See NOTES */

errnum:
The value of the error number of the passed in parameter is generally the value of errno

Return value:
Cause of error

#include <stdio.h>

#include <errno.h>

void perror(const char *s);   /* See NOTES */

s:
Pass in parameters, custom description

Return value:
nothing

Output the error reason to stderr

Keywords: Linux socket Ubuntu

Added by killsite on Tue, 25 Jan 2022 09:47:04 +0200