Simple implementation of a socket server and client under linux

table of Contents

1. Linux file descriptor

2. Create a socket under Linux

3. The bind() function and connect() function

3.1, bind() function

3.2, connect() function

4. The listen() function and accept() function

4.1, listen() function

4.2, accept() function

5、write()和read()

5.1, write() function

5.2, read() function

6, send() and recev()

7, a simple implementation of service and client


1. Linux file descriptor

In Linux, everything is a file. A hardware device can also be mapped to a virtual file, called a device file. For example, stdin is called a standard input file, and its corresponding hardware device is generally a keyboard, stdout is called a standard output file, and its corresponding hardware device is generally a display. For all files, you can use the read() function to read data and the write() function to write data.

The idea of ​​"everything is a file" greatly simplifies the programmer's understanding and operation, making the processing of hardware devices just like ordinary files. All files created in Linux have an int type number called File Descriptor. When using a file, you only need to know the file descriptor. For example, the descriptor for stdin is 0 and the descriptor for stdout is 1.

In Linux, sockets are also considered to be a kind of file, which is no different from ordinary file operations, so functions related to file I/O can naturally be used in the process of network data transmission. It can be considered that the communication between two computers is actually the mutual reading and writing of two socket files.

File descriptors are sometimes called File Handle, but "handle" is mainly a term in Windows.

2. Create a socket under Linux

Under Linux, use the socket() function in the <sys/socket.h> header file to create a socket. The prototype is:

int socket(int af, int type, int protocol);
  • af: Address Family, that is, IP address type, commonly used AF_INET and AF_INET6. AF is short for "Address Family" and INET is short for "Internet". AF_INET represents an IPv4 address. For example, 127.0.0.1; AF_INET6 represents an IPv6 address, such as 1030::C9B4:FF12:48AA:1A2B. You can also use the PF prefix. PF is short for "Protocol Family", which is the same as AF. For example, PF_INET is equivalent to AF_INET, and PF_INET6 is equivalent to AF_INRT6.
  • type: data transmission method, commonly used are SOCK_STREAM and SOCK_DGRAM .
  • Protocol represents the transmission protocol. The commonly used ones are IPPROTO_TCP and IPPTOTO_UDP, which represent TCP transmission protocol and UDP protocol respectively.

Seeing this, you may have a question. With the IP address type and data transmission method, is it not enough to decide which protocol to use? Why is the third parameter needed?

That’s right, under normal circumstances, you can create a socket with two parameters af and type, and the operating system will automatically deduce the protocol type, unless you encounter such a situation: there are two different protocols that support the same IP address type And data transmission methods. If we don’t know which protocol to use, the operating system cannot automatically deduct it.

If the value of af is set to PF_INET and the SOCK_STREAM transmission method is used, then the only protocol that meets these two conditions is TCP, so the SOCK() function can be called like this:

int tcp_socket = socket(AD_INET,SOCK_STREAM,IPPROTO_TCP);  //TCP套接字

If the value of af is set to PF_INET and the SOCK_DGRAM transmission method is used, then the only protocol that meets these two conditions is UDP, so the SOCKET() function can be called like this:

int udp_socket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); //UDP套接字

In the above two cases, only one protocol satisfies the conditions. You can set the value of protocol to 0, and the system will automatically deduce which protocol should be used. The code is as follows

int tcp_socket = socket(AD_INET,SOCK_STREAM,0);  //TCP套接字

int udp_socket = socket(AF_INET,SOCK_DGRAM,0); //UDP套接字

3. The bind() function and connect() function

The socket() function is used to create a socket, determine various attributes of the socket, and then the server side needs to use the bind() function to bind the socket to a specific IP address and port. Only in this way, it flows through the The data of the IP address and port can be handed over to the socket; the client needs to use the connect() function to establish a connection.

3.1, bind() function

The prototype of the bind() function is:

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

sock is the socket file descriptor, addr is the sockaddr structure variable pointer, and addrlen is the size of the addr variable.

The definition of socklen_t is  actually a uint32.

Let's look at a code that binds the created socket to the IP address 128.0.0.1 and port 1123:

int serv_sock = sock(AF_INET,SOCK_STREAM,IPPROTO);  //创建一个TCP套接字

struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(servaddr));
serv_addr.sin_famil = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1123);  //端口

bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr));

Let's take a look at the sockaddr_in structure

struct sockaddr_in
{
    sa_family_t    sin_family;  //地址族(Address Family),也就是地址类型
    unit16_t       sin_port;    //16位的端口号
    struct in_addr sin_addr;   //32位IP地址
    char           sin_zero[8]; //不使用,一般用0填充
}
  1. The meaning of the first parameter of sin_family and socket() is the same, and the value should be the same.
  2. sin_port is the port number, and the length of uint16_t is two bytes. Theoretically, the value range of the port number is 0~65536, but the port 0~1023 is generally assigned to a specific service program by the system, for example, the port of web service is 70 , The port of the FTP service is 21, so our program tries to allocate port numbers between 1024~65536.
  3. sin_addr is a variable of struct in_addr structure type.
  4. sin_zero is more than 8 bytes, which is useless, and is generally filled with 0 using the memset() function. In the above code, first use memset() to fill all bytes of the structure with 0, and then assign values ​​to the first three members. The remaining sin_zero is naturally 0.

in_addr structure

The third member of sockaddr_in is a structure of type in_addr, which contains only one member, as shown below:

struct in_addr
{
    in_addr_t s_addr;  //32位的IP地址
};

in_addr_t is defined in the header file <netinet/in.h>, which is equivalent to unsigned long and has a length of four bytes. In other words, s_addr is an integer, and the IP address is a string, so the inet_addr() function is needed for conversion, for example:

unsigned long ip = inet_addr("127.0.0.1");

operation result:

Why use sockaddr_in instead of sockaddr?

The type of the second parameter of bind() is sockaddr, but sockaddr_in is used in the code, and then it is forced to sockaddr.

The definition of the sockaddr structure is as follows:

struct sockaddr
{
    sa_family_t sin_family;   //地址族
    char        sa_data[14];  //IP地址和端口号
}

The figure below is a comparison of sockaddr and sockaddr_in (the number in parentheses indicates the number of bytes occupied)

The length of sockaddr and sockaddr_in is the same, both are 16 bytes, but the sa_data area of ​​sockaddr needs to specify the IP address and port number at the same time, such as "127.0.0.1:8080". Unfortunately, there is no related function to convert this string to the required It is difficult to assign values ​​directly to variables of type sockaddr, so use sockaddr_in instead. The length of these two structures is the same, and no bytes will be lost when the type is cast, and there will be no more bytes.

It can be considered that sockaddr is a general structure that can be used to protect multiple types of IP addresses and port numbers, while sockaddr_in is a structure specifically used to store IPv4. In addition, there is sockaddr_in6, which is used to save the IPv6 address. Its definition is as follows:

struct sockaddr_in6
{
    sa_family_t sin6_family;   //IP地址类型,取值为AF_INET6
    in_port_t   sin6_port;     //16位端口号
    uint32_t sin6_flowinfo;    //IPv6流信息
    struct in6_addr sin6_addr; //具体的IPv6地址
    unit32_t sin6_scpoe_id;    //接口范围ID
};

The sockaddr_in and sockaddr_in6 declared in in.h are as follows: 

3.2, connect() function

The connect() function is used to establish a connection, and its prototype is:

int connect(int sock,struct sockaddr *serv_addr,struct sockaddr*serv_addr,socklen_t addrlen);

The description of each parameter is the same as the bind() function.

4. The listen() function and accept() function

For the server-side program, after using bind() to bind the socket, you also need to use the listen() function to make the socket enter the passive listening state, and then call the accept() function to respond to the client's request at any time.

4.1, listen() function

The listen() function allows the socket to enter the passive listening state. Its prototype is:

int listen(int sock,int backlog)

sock is the socket that needs to enter the listening state, and backlog is the maximum length of the request queue

The so-called passive monitoring means that the socket is in a "sleep" state when there is no client request. Only when a client request is received, the socket will be "awakened" to respond to the request.

Request queue

When the socket is processing a client request, if a new request comes in, the socket cannot be processed, it can only be placed in the buffer first, and then from the buffer after the current request is processed Read it out for processing. If there are new requests coming in, they will be queued in the buffer in order until the buffer is full. This buffer is called the request queue.

The length of the buffer (how many client requests can be stored) can be specified by the backlog parameter of the listen() function, but there is no standard for how much it is, and it depends on your needs.

If the value of backlog is set to SOMAXCONN, the system determines the length of the request queue. This value is generally relatively large, and may be several hundred or more.

When the request queue is full, no new requests are received. For linux, the client will receive an ECONNREFUSED error.

Note: The listen() function just keeps the socket in the listening state, and does not receive the request. To receive a request, you need to use the accept() function to block the execution of the process until a new request comes.

4.2, accept() function

When the socket is in the listening state, the client request can be received through the acceot() function. Its prototype is:

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

Its parameters are the same as listen() and connect(); sock is the server-side socket, addr is the sockaddr_in structure variable, and addrlen is the length of the parameter addr, which can be obtained by sizeof().

accept() returns a new socket to communicate with the client, addr saves the client's IP address and port number, and sock is the server-side socket. When communicating with the client later, use this newly generated socket instead of the original server-side socket.

5、write()和read()

Linux does not distinguish between socket files and ordinary files. Use write() to write data to the socket, and use read() to read data from the socket.

Communication between two computers is equivalent to communication between two sockets. Use write() on the server to write data to the socket, and the client can receive it, and then use read() to connect from the socket. After reading out the word, a communication is completed.

5.1, write() function

The prototype of write() is:

/*
fd:待写入的文件的描述符
buf:待写入的数据的缓冲区地址
nbytes:写入的数据的字节数
ssize_t:signed int
*/
ssize_t write(int  fd,const void *buf,size_t nbytes);

The write() function will write the nbytes bytes in the buffer buf to the file fd. If it succeeds, it returns the number of bytes written, and if it fails, it returns -1.

5.2, read() function

/*
fd:待读取的文件的描述符
buf:待读取的数据的缓冲区地址
nbytes:读取的数据的字节数
ssize_t:signed int
*/
ssize_t read(int  fd,void *buf,size_t nbytes);

The read() function reads nbytes bytes from the fd file and saves them in the buffer buf. It returns the number of bytes read if it succeeds (returns 0 when it encounters the end of the file), and returns -1 if it fails.

6, send() and recev()

/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
                       int __flags, __CONST_SOCKADDR_ARG __addr,
                       socklen_t __addr_len);

/* Read N bytes into BUF through socket FD.
   If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
   the sender, and store the actual size of the address in *ADDR_LEN.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
                         int __flags, __SOCKADDR_ARG __addr,
                         socklen_t *__restrict __addr_len);


/* Send a message described MESSAGE on socket FD.
   Returns the number of bytes sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendmsg (int __fd, const struct msghdr *__message,
                        int __flags);

#ifdef __USE_GNU
/* Send a VLEN messages as described by VMESSAGES to socket FD.
   Returns the number of datagrams successfully written or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int sendmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags);
#endif

/* Receive a message as described by MESSAGE from socket FD.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvmsg (int __fd, struct msghdr *__message, int __flags);

#ifdef __USE_GNU
/* Receive up to VLEN messages as described by VMESSAGES from socket FD.
   Returns the number of messages received or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int recvmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags,
                     struct timespec *__tmo);
#endif

7, a simple implementation of service and client

/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:service.c
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

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

int main(){
    //创建套接字
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //将套接字和IP、端口绑定
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //进入监听状态,等待用户发起请求
    listen(serv_sock, 20);

    //接收客户端请求
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    while(1)
    {
        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

        //向客户端发送数据
        char str[] = "不要艾特我";
        write(clnt_sock, str, sizeof(str));

        //关闭套接字
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}
/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:client.cpp
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(){
    //创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //读取服务器传回的数据
    char buffer[40];
    read(sock, buffer, sizeof(buffer)-1);

    printf("Message form server: %s\n", buffer);

    //关闭套接字
    close(sock);

    return 0;
}

operation result:

Start server

server is in monitoring state

Start the client:

At this point, a simple socket communication code is completed

 

 

 

 

 

 

 

Guess you like

Origin blog.csdn.net/weixin_40179091/article/details/113024907