[TCP/IP Network Programming] The most complete actual combat essence notes 2 - address family and TCP server/client

Address Family and Data Sequence


The IP and port assigned to the socket

IPV4Belongs to 4-byte address family
IPV6Belongs to 16-byte address family

Range of first byte of class A address: 0-127
Range of first byte of class B address: 128-191
Range of first byte of class C address: 192-223

When the network interface card (NIC) inside the computer transmits data, it will have a port number attached, and the computer will transfer it to the corresponding socket according to the port number

Port number characteristics

  • Unable to assign a port number to different sockets
  • The port number can be allocated in the range of 0-65535
  • 0-1023 are well-known ports, assigned to specific applications
  • TCP sockets and UDP sockets do not share port numbers

address information representation

sockaddr_in

This structure defines the format of the socket address, which is used to specify the network address of the server and client in socket programming

struct sockaddr_in {
    
    
    sa_family_t     sin_family;   // 地址族,通常为 AF_INET(IPv4);AF_INET6(IPV6)
    uint16_t        sin_port;     // 16 位端口号,使用网络字节序(大端序)
    struct in_addr  sin_addr;     // 表示 32 位 IP 地址的结构体
    char            sin_zero[8];  // 不使用,填充字节,通常为全零
};

General structure sockaddr

This structure is generic, not just for IPV4

struct sockaddr {
    
    
    sa_family_t   sin_family;   // 地址族,用于指定地址的类型
    char          sa_data[14];  // 地址信息,具体格式取决于地址族的类型
};

Network byte order and address translation

Byte order and network byte order

There are two ways for the CPU to save data to memory:

  • Big-endian: high-order bytes are stored in low-order addresses. The network byte order is big endian.
  • Little endian: high-order bytes are stored in high-order addresses. Currently mainstream Intel series CPUs store data in little-endian order.

The data array is first converted into a unified big-endian format for network transmission——network byte order and
little-endian system should be converted to big-endian format when transmitting data

The received data is stored in little endian


endian conversion

Here's a function that converts endianness for a few sights:htons、ntohs、htonl、ntohl

Now analyze htons:

  • h indicates the host host, byte order
  • to means converted to
  • n represents the network network, byte order
  • s means short, and the corresponding l means long

Except for filling data into the sockaddr_in structure variable, the byte order problem does not need to be considered in other cases


Initialization and allocation of network addresses

We need to change the IP address form (dotted decimal) we often see to the 32-bit integer data accepted by sockaddr_in

Here are two functions to achieve this

inet_addr

The conversion type comes with network byte order conversion, and can also identify wrong IP addresses

#include <arpa/inet.h>
in_addr_t inet_addr(const char* string);

inet_aton

He uses inet_addrthe structure, which is more efficient

It accepts a inet_addrpointer, and saves the data in the original way to the pointer after running

#include <arpa/inet.h>
int inet_aton(const char* string, struct in_addr* addr);

As the name suggests, you can get inet_ntoa is a reverse conversion


Network address initialization

struct sockaddr_in addr;                             // 声明一个 sockaddr_in 结构体变量 addr
char *serv_ip = "211.217.168.13";                     // 声明 IP 地址字符串
char *serv_port = "9190";                             // 声明端口号字符串
memset(&addr, 0, sizeof(addr));                       // 将 addr 的所有成员初始化为 0,主要是为了将 sockaddr_in 的成员 sin_zero 初始化为 0。
addr.sin_family = AF_INET;                            // 指定地址族为 AF_INET,表示 IPv4 地址族
addr.sin_addr.s_addr = inet_addr(serv_ip);            // 使用 inet_addr 函数将字符串形式的 IP 地址转换为二进制形式,并将结果存储在 sin_addr.s_addr 中
addr.sin_port = htons(atoi(serv_port));               // 使用 atoi 函数将端口号字符串转换为整数,并使用 htons 函数将端口号转换为网络字节序(大端序),然后存储在 sin_port 中

Of course, if you find it troublesome, you can avoid entering the IP address every time and use INADDR_ANYinstead

Using this method, you can automatically obtain the IP address of the host running the server (this method is preferred for the server)


Based on TCP server and client


Understand TCP/UDP

For this part, you can refer to the summary notes corresponding to the computer network, or the corresponding face-to-face experience, and no specific introduction will be made here


Implement TCP-based server and client

TCP server default function call order

The following is the corresponding default function call sequence

  1. socket() creates a socket
  2. bind() assigns socket address
  3. listen() waits for connection request status
  4. accept() allows the connection
  5. read()/write() data exchange
  6. close() disconnects the link

Enter the waiting connection request state

Before calling the accept function, the request will be in a waiting state

The server is in the state of waiting for connection requests, which means that the requests from the client are in the waiting state, so as to wait for the server to accept their requests.

Link waiting queue: unaccepted connection requests are placed here, the size of the connection pool depends on the size defined by the backlog

#include <sys/socket.h>

// 返回值:成功时返回 0,失败时返回 -1
// 参数一:传递文件描述符套接字的用途
// 参数二:等待队列的最大长度
int listen(int sockfd, int backlog);

Accept client link requests

#include <sys/socket.h>

// sockfd:服务器套接字的文件描述符;sockaddr:用于保存发起连接请求的客户端地址信息;addrlen:第二个参数的长度。
// 返回值:成功时返回创建的套接字文件描述符,失败时返回 -1
int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);

The server socket just controls whether connection requests are allowed to enter the server

acceptThe function will accept the pending client connection request in the connection request waiting queue, it will take out a connection request from the waiting queue, create a socket and complete the connection request. If the waiting queue is empty, the accpet function will block and will not return until a new connection request appears in the queue.

After accept is executed, it will store the client address information corresponding to the connection request it accepts into the second parameter addr.

After the accept function is called successfully, it will generate a new socket internally and return its file descriptor, which is used to establish a connection with the client and perform data I/O.


client connection request

The client default function call sequence is

  1. socket() creates a socket
  2. connect() request connection
  3. read()/write() exchange data
  4. close() disconnects the link

#include <sys/socket.h>

// 参数:sock:客户端套接字的文件描述符;serv_addr:保存目标服务器端地址信息的结构体指针;addrlen:第二个参数的长度(单位是字节)
// 返回值:成功时返回 0,失败时返回 -1
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);

The client connectwill block after calling the function, and will not return until one of the following conditions occurs:

  • The server side receives the connection request.
  • The connection request is interrupted due to abnormal conditions such as network disconnection.

Note: Data exchange is not performed immediately after the connect function returns


TCP-based server-side and client-side function call relationship

insert image description here

listenThe client can only call the function after the server calls connectthe function


Implement iterative server-side and client-side

Iterative server-side implementation

Repeatedly calling the accept function can realize the continuous acceptance of client link requests

At present, we can only realize: after accepting one client, accept the next one, and cannot accept multiple clients at the same time (this technique will be introduced later)


Iterate echo server and client

Basic mode of operation:

  1. The server is only connected to one client at a time and provides echo service.
  2. The server side provides services to 5 clients in turn, and then exits.
  3. The client receives the string entered by the user and sends it to the server.
  4. The server sends the received string data back to the client, that is, "echo".
  5. The string echo between server and client continues until the client enters Q.

The echo client is perfectly implemented

Generally, it is impossible for the server to know the length of the data sent by the client in advance, so the data boundary and the corresponding length can only be determined through the application layer protocol

The application layer protocol is actually a collection of rules that are gradually defined during the implementation of the server/client.

The following shows the code of the counter server and the counter client in turn

// 客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 50
#define OPSZ 4 // 定义每个操作数在 TCP 报文中占用的字节数

void error_handling(char *message);

int main(int argc, char *argv[])
{
    
    
    int sock;
    char opmsg[BUF_SIZE]; // opmsg 用来存储要发送的数据,注意是 char 类型数组
    struct sockaddr_in serv_addr;
    int operand_count, result;

    if (argc != 3)
    {
    
    
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)))
        error_handling("connect() error");
    else
        puts("Connecting..........\n");

    fputs("Operand count: ", stdout);
    scanf("%d", &operand_count);
    opmsg[0] = (char)operand_count; // 数据的第一个字节存储操作数的数量,注意要将变量类型转换为 char。

    for (int i = 0; i < operand_count; i++)
    {
    
    
        printf("Operand %d: ", i + 1);
        scanf("%d", (int *)&opmsg[i * OPSZ + 1]); // 从第二个字节开始每四个字节存储一个操作数,向数组存数据时先取地址再转换类型。
    }

    fgetc(stdin);
    fputs("Operator: ", stdout);
    scanf("%c", &opmsg[operand_count * OPSZ + 1]); // 再用一个字节存储运算符

    write(sock, opmsg, operand_count * OPSZ + 2); // 发送数据
    read(sock, &result, OPSZ);                    // 接收运算结果:运算结果是一个 4 字节的操作数

    printf("Operation result: %d\n", result);

    close(sock);
    return 0;
}

void error_handling(char *message)
{
    
    
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

// 服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 50
#define OPSZ 4
void error_handling(char *message);
int calculate(int operand_count, int operands[], char operator);

int main(int argc, char *argv[])
{
    
    
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    int clnt_addr_sz;
    char message[BUF_SIZE];

    if (argc != 2)
    {
    
    
        printf("Usage : %s <port>", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");

    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    clnt_addr_sz = sizeof(clnt_addr);
    for (int i = 0; i < 5; i++)
    {
    
    
        if ((clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_sz)) == -1)
            error_handling("accept() error");

        int operand_count;
        read(clnt_sock, &operand_count, 1); // 首先读取第 1 个字节,获取操作数的数量

        char operands[BUF_SIZE];
        for (int i = 0; i < operand_count; i++)
        {
    
    
            read(clnt_sock, &operands[i * OPSZ], OPSZ); // 根据操作数数量,依次读取操作数
        }

        char operator;
        read(clnt_sock, &operator, 1); // 读取运算符

        int result = calculate(operand_count, (int *)operands, operator);
        write(clnt_sock, (char *)&result, sizeof(result)); // 发送计算结果
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}

int calculate(int operand_count, int operands[], char operator)
{
    
    
    int result = operands[0];
    switch (operator)
    {
    
    
    case '+':
        for (int i = 1; i < operand_count; i++)
            result += operands[i];
        break;
    case '-':
        for (int i = 1; i < operand_count; i++)
            result -= operands[i];
        break;
    case '*':
        for (int i = 1; i < operand_count; i++)
            result *= operands[i];
        break;
    }
    return result;
}

void error_handling(char *message)
{
    
    
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

TCP principle

Call the write function, the data is first moved into the output buffer, and then transferred to the input buffer of the other party at an appropriate time;

Call the read function to read the content in the input buffer;

The IO characteristics corresponding to the socket

  1. I/O buffering exists individually for each socket.
  2. I/O buffers are generated automatically when sockets are created.
  3. Data left in the output buffer will continue to be delivered even if the socket is closed.
  4. Closing the socket will lose data in the input buffer.

Guess you like

Origin blog.csdn.net/delete_you/article/details/132032390