TCP socket communication

TCP socket communication

How do processes communicate in the network?

We know that there are many ways to communicate between local processes, such as pipeline, message queue, shared memory, synchronization and mutual exclusion. These methods require that the two processes of communication are located on the same host. So how to communicate between networks? The process PID can be used to uniquely identify a process locally, but it is not feasible in the network. Through the knowledge of TCP/IP, we know that the "ip address" of the network layer can uniquely identify the host in the network, and the "port number" of the transport layer can uniquely identify the host's application. In this way, we can uniquely identify two processes in the network by using the combination of IP address and port number, thus realizing communication.

The concept of sockets?

Two programs on the network exchange data through a two-way communication connection, one end of which is called a socket. Establishing a network communication connection requires at least one pair of port numbers (sockets). Socket is essentially a programming interface (API). For TCP/IP encapsulation, TCP/IP also provides an interface for programmers to do network development, which is Socket programming interface; HTTP is a car, which provides a specific form of encapsulation or display of data; Socket is an engine, which provides the ability of network communication. Socket means "hole" or "socket" in English. As BSD UNIX Process Communication Mechanisms, take the latter meaning. Usually referred to as " socket ", used to describe IP addresses and ports, is a communication chain handle, can be used to achieve communication between different virtual machines or different computers. On the Internet Host Generally, there are several service software running, and several services are provided at the same time. Each service opens a Socket and binds to a port. Different ports correspond to different services. Simply understood, sockets are ip addresses plus port numbers.

Network byte order

In order to achieve communication in the network, data transmission is indispensable. So the concept of network byte order is introduced here. In the previous study, we came into contact with the concepts of the big end and the small end. Small end: the low end of the data in the low address, high in the high address; large end: the low end of the data in the high address, high in the low address. Network data flow can also be divided into large-end and small-end. Network data streams send out low addresses first, and then high addresses. TCP/IP stipulates that the network data stream adopts large-end byte order, that is, low-end and high-address. Why do we talk about the big end and the small end? Because the port number must be known when communicating with the network. If the sender is a big-end byte order and the receiver is a small-end byte order, the port number that we finally see is the incorrect port number. Therefore, we must convert the port number into a uniform byte order form between the sender and the receiver.

Conversion interface:

(1) Single-process socket communication

a. Calling socket functions to create sockets

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

The socket function corresponds to the open operation of the ordinary file. The normal file opening operation returns a file descriptor, and socket() is used to create a socket descriptor, which uniquely identifies a socket. This socket descriptor, like the file descriptor, is used in subsequent operations. It is used as a parameter to perform some reading and writing operations.

When creating a socket, you can also specify different parameters to create different socket descriptors. The three parameters of the socket function are:

Domain: the protocol domain, also known as the protocol family. Commonly used protocol families are AF_INET, AF_INET6, AF_LOCAL (or AF_UNIX, Unix domain socket), AF_ROUTE and so on. The protocol family decides the address type of socket, and the corresponding address must be used in communication. For example, AF_INET decides the combination of ipv4 address (32 bits) and port number (16 bits), AF_UNIX decides to use an absolute path name as the address.

Type: Specify the socket type. Commonly used socket types are SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_PACKET, SOCK_SEQPACKET, etc. (What are socket types?).

Protocol: Specify the protocol. The commonly used protocols are IPPROTO_TCP, IPPTOTO_UDP, IPPROTO_SCTP, IPPROTO_TIPC and so on. They correspond to TCP, UDP, STCP and TIPC respectively. Note: The above type and protocol can not be combined at will, such as SOCK_STREAM can not be combined with IPPROTO_UDP. When protocol is 0, the default protocol corresponding to type type type is automatically selected. When we call socket to create a socket, the returned socket descriptor exists in the address family (AF_XXX) space, but there is no specific address. If you want to assign an address to it, you must call the bind() function. Otherwise, when you call connect() and listen(), the system will automatically randomly allocate a port.

b. Binding local and port: bind() function

The bind() function assigns a specific address in an address family to socket. For example, corresponding to AF_INET and AF_INET6, an ipv4 or ipv6 address and port number combination is assigned to socket.

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

The three parameters of the function are:

sockfd: socket descriptor, which is created through the socket() function and uniquely identifies a socket. The bind() function binds a name to the descriptor.

addr: A const struct sockaddr * pointer to the protocol address to be bound to sockfd. This address structure varies according to the address protocol family when the address creates the socket. For example, ipv4 corresponds to:

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 */

};

addrlen: corresponds to the length of the address.

c, listen(), connect() functions

If as a server, after calling socket() and bind(), listen() is called to listen for the socket. If the client calls connect() to make a connection request at this time, the server will receive the request.

int listen(int sockfd, int backlog);

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

The first parameter of the listen function is the socket descriptor to listen on, and the second parameter is the maximum number of connections that the corresponding socket can queue. The socket created by the socket() function defaults to an active type, and the listen function changes the socket to a passive type, waiting for the client's connection request.

The first parameter of the connect function is the socket descriptor of the client, the second parameter is the socket address of the server, and the third parameter is the length of the socket address. The client establishes a connection with the TCP server by calling the connect function.

d, accept() functions

After the TCP server calls socket(), bind(), listen(), the specified socket address will be monitored. After calling socket() and connect() in turn, the TCP client sends a connection request to the TCP server. After the TCP server hears the request, it calls the accept() function to receive the request, so that the connection is established. Then you can start the network I/O operation, which is similar to the read-write I/O operation of ordinary files.

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

The first parameter of the accept function is the socket descriptor of the server, the second parameter is the pointer to struct sockaddr* to return the protocol address of the client, and the third parameter is the length of the protocol address. If accpet succeeds, its return value is a completely new descriptor automatically generated by the kernel, representing the TCP connection to the returning customer. Note: The first parameter of accept is the socket descriptor of the server, which is generated by the server starting to call the socket() function, called the listening socket descriptor, while the accept function returns the connected socket descriptor. A server usually only creates a listener socket descriptor, which persists throughout the server's lifetime. The kernel creates a connected socket descriptor for each customer connection accepted by the server process. When the server completes its service to a customer, the corresponding connected socket descriptor is closed.

read reads the data in the socket. When communication is complete, call close to close the socket.

Code implementation:

//server.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int StartUp(int port,const char* ip)
{
    int ListenSock = socket(AF_INET,SOCK_STREAM,0);
    if(ListenSock < 0)
    {
        perror("socket");
        exit(1);
    }
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip);
    if(bind(ListenSock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        exit(2);
    }
    if(listen(ListenSock,5) < 0)
    {
        perror("listen");
        exit(3); 
    }
    return ListenSock;
}

int main(int argc,const char* argv[])
{
    if(argc != 3)
    {
        printf("input error\n");
        return 1;
    }
    int len;
    int listenSock = StartUp(atoi(argv[2]),argv[1]);
    struct sockaddr_in client;
    while(1)
    {
        int sock = accept(listenSock,(struct sockaddr*)&client,&len);//Getting information from clients
        if(sock < 0)
        {
            perror("accept");
            continue;
        }
        printf("get a client,ip is %s,port is %d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
        char buf[1024];
        while(1)
        {
            ssize_t s = read(sock,buf,sizeof(buf)-1);//Server reads data
            if(s > 0)
            {
                buf[s] = 0;
                printf("client# %s\n",buf);
            }
            else
            {
                //The data has been read out, and the client does not send the data.
                printf("client is quit!\n");
            }
        }
        close(sock);
    }
    return 0;
}

//client.c
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>

int main(int argc,const char* argv[])
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        return 1;
    }
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
    {
        perror("connect");
        return 2;
    }
    char buf[1024];
    while(1)
    {
        printf("send# ");
        fflush(stdout);
        //Read data from standard input, read to buf, and write from buf to pipeline
        ssize_t s = read(0,buf,sizeof(buf)-1);
        if(s < 0)
        {           
            perror("read");
            return 3;
        }       
        buf[s-1] = 0;
        write(sock,buf,s);
    }
    close(sock);
    return 0;
}

(2) Multiprocess socket communication

The code above can only send data to one client, but in practical applications, there will be multiple clients sending data to the server, so the implementation above is not practical. Therefore, we can implement a multi-process socket communication to achieve multiple clients to send data to the server.

Implementation: The server can create multiple sub-processes to process the information sent by the client. Every time we receive a connection request from a new client, we fork() a child process, which is used to wait for the child process, and the child process which is used to read the data sent by the client. Carefully, you may find that we also fork() once before the child process reads the information. Why? In fact, we use the child process fork() to create a grandson process, terminate the son process, the son process is recycled by its father process, at this time the grandson process is an orphan process, adopted by process 1. The goal is not to let the son process wait too long for the grandson process and consume too much system resources.

Code implementation:

//server.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int StartUp(int port,const char* ip)
{
    int ListenSock = socket(AF_INET,SOCK_STREAM,0);
    if(ListenSock < 0)
    {
        perror("socket");
        exit(1);
    }
    int opt = 1;
    setsockopt(ListenSock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip);
    if(bind(ListenSock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        exit(2);
    }
    if(listen(ListenSock,5) < 0)
    {
        perror("listen");
        exit(3);
    }
    return ListenSock;
}

int main(int argc,const char* argv[])
{
    if(argc != 3)
    {
        printf("input error\n");
        return 1;
    }
    int len;
    int listenSock = StartUp(atoi(argv[2]),argv[1]);
    struct sockaddr_in client;
    while(1)
    {
        int sock = accept(listenSock,(struct sockaddr*)&client,&len);//Getting information from clients
        if(sock < 0)
        {
            perror("accept");
            continue;
        }

        printf("get a client,ip is %s,port is %d\n",inet_ntoa(client.sin_addr),\
                    ntohs(client.sin_port));
        int id = fork();
        if(id > 0)
        {
            close(sock);
            while(waitpid(-1,NULL,WNOHANG) > 0);
            continue;
        }
        else
        {
            close(listenSock);
            if(fork() > 0)
            {
                exit(0);
            }
            char buf[1024];
            while(1)
            {
                ssize_t s = read(sock,buf,sizeof(buf)-1);//Server reads data
                if(s > 0)
                {
                    buf[s] = 0;
                    printf("client# %s\n",buf);
                }
                else
                {
                    //The data has been read out, and the client does not send the data.
                    printf("client is quit!\n");
                    break;
                }
            }
            close(sock);
        //    exit(4);
            break;
        }
    }
    return 0;
}
//client.c
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>

int main(int argc,const char* argv[])
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        return 1;
    }
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
    {
        perror("connect");
        return 2;
    }
    char buf[1024];
    while(1)
    {
        printf("send# ");
        fflush(stdout);
        //Read data from standard input, read to buf, and write from buf to pipeline
        ssize_t s = read(0,buf,sizeof(buf)-1);
        if(s < 0)
        {           
            perror("read");
            break;
        }       
        buf[s-1] = 0;
        write(sock,buf,s);
    }
    close(sock);
    return 0;
}

(3) Multithread Socket Communication

In the learning of system programming, we know that threads are an execution flow within a process and run in the address space of the process. The process is a dynamic execution process of the program. If there are too many processes in the system, it will increase the burden of the system. So here we use threads to communicate.

Method of realization:

A new thread is created in the main thread. The execution function of the new thread is to read information. Similar to the above multi-process communication, we can separate new threads. After separation, threads do not need to wait for the main thread, but are recycled by the operating system area. (We can't join new threads here. If we do, the main threads will still have to wait a long time, so the new threads will still be recycled by the system.)

Code implementation:

 //server.c
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<sys/socket.h>
int StartUp(int port,const char* ip)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        exit(2);
    }
    int opt = 1;
    setsockopt(ListenSock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip);
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        exit(3);
    }
    if(listen(sock,5) < 0)
    {
        perror("listen");
        exit(4);
    }
    return sock;
}
void* thread_hander(void* arg)
{
    int sock = *((int*)arg);
    char buf[1024];
    while(1)
    {
        ssize_t _s = read(sock,buf,sizeof(buf)-1);
        if(_s > 0)
        {
            buf[_s-1] = 0;
            printf("client say#%s\n",buf);
            if(write(sock,buf,sizeof(buf)-1)<0)
            {
                break;
            }
        }
        else if(_s == 0)
        {
            printf("client is quit!\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    close(sock);
}
int main(int argc,const char* argv[])
{
    if(argc != 3)
    {
        printf("input  error\n");
        return 1;
    }
    int listenSock = StartUp(atoi(argv[2]),argv[1]);
    struct sockaddr_in client;
    int len = 0;
    while(1)
    {
        int sock = accept(listenSock,(struct sockaddr*)&client,&len);
        if(sock < 0)
        {
            perror("accept");
            return 5;
        }
        printf("get a client!ip is %s,port is %d\n",inet_ntoa(client.sin_addr),\
                ntohs(client.sin_port));
        pthread_t tid;
        int ret = pthread_create(&tid,NULL,thread_hander,&sock);
        if(ret < 0)
        {
            perror("pthread_create");
            return 6;
        }
        pthread_detach(tid);
    }
    return 0;
}
//client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main(int argc, const char* argv[])
{
    if(argc != 3)
    {
        printf("input error\n");
        return 1;
    }
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        return 2;
    }
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    int ret = connect(sock,(struct sockaddr*)&server,sizeof(server));
    if(connect < 0)
    {
        perror("connect");
        return 3;
    }
    char buf[1024];
    while(1)
    {
        printf("send#");
        fflush(stdout);
        ssize_t _s = read(0,buf,sizeof(buf)-1);
        if(_s > 0)
        {
            buf[_s - 1] = 0;
            if(write(sock,buf,sizeof(buf)-1) < 0)
            {
                break;
            }
            ssize_t s = read(sock,buf,sizeof(buf)-1);
            if(s > 0)
            {
                buf[s] = 0;
                printf("server echo#%s\n",buf);
            }
        }
        else
        {
            perror("read");
            return 4;
        }
    }
    return 0;
}

Keywords: socket network Programming Unix

Added by knnakai on Mon, 10 Jun 2019 05:01:42 +0300