C language----detailed explanation of socket communication

1: What is socket?

        Students who are new to socket must also know the Chinese name of socket, socket. It is not so much the Chinese name as what it is. Let’s not worry about the actual meaning of the Chinese name. Let’s first understand what socket is.

        The data we generate online is encapsulated layer by layer through the protocol stack and then sent to the network through the network card. It is sent to the server through the network, and then the server decapsulates it layer by layer to get the data it wants.

        The protocol stack is integrated in the operating system. We don't need to care about how TCP, UDP and other protocols are implemented. What we care about is whether the data of our application can be sent out normally and received back from the server. data. This requires a bridge, one end connected to the protocol stack of the operating system, and the other end connected to the user's application data. Socket is this bridge.

        Then let’s understand the Chinese name socket again. After looking around, the explanation I most agree with is: socket refers to a socket pipe, which is a pipe that connects two water pipes, and the "word" is the data of this connection. The identifier is a WORD, so the socket is a data body that identifies the connection.

        Some students have questions about what WORD is. In Linux and other systems, "socket" corresponds to "socket word", so "word" also corresponds to "word". This "word" may refer to the data identifier of the stored socket, because The port number is two bytes, which is a WORD .

        The picture below is very concrete and not as abstract as the one above.

        That’s it for the explanation of sockets. I really can’t go on.

Two: socket communication process

        Just like the TCP connection process, TCP link establishment requires three handshakes and TCP link removal requires four waves. Socket communication also has its own set of processes.

For clients:

1. Create a socket (fd) for communication

2. To connect to the server, you need to specify the IP and port of the connected server.

3. The connection is established successfully, and the client and server establish a connection channel.

        1> can send data

        2> can receive data

4. Communication ends, disconnect

For the server:

1. Create a socket for listening

        1>Listening: Listening for client connections

        2>Socket: This socket is actually a file descriptor

2. Bind this listening file descriptor to the local IP and port (the IP and port are the address information of the server)

        1>The client uses this IP and port when connecting to the server

3. Set up monitoring, and the monitoring fd starts to work.

4. Blocking and waiting. When a client initiates a connection, unblock and accept the client's connection, you will get a socket (fd) to communicate with the client.

5. Communication between server and client

        1>Receive data

        2>Send data

6. Communication ends, disconnect

Three: Detailed explanation of socket communication function

1. socket() function

int socket(int domain, int type, int protocol); 
        - 功能:创建一个套接字 
        - 参数: 
                - domain: 协议族 
                        AF_INET : ipv4 
                        AF_INET6 : ipv6 
                        AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信) 
                - type: 通信过程中使用的协议类型 
                        SOCK_STREAM : 流式协议 
                        SOCK_DGRAM : 报式协议 
                - protocol : 具体的一个协议。一般写0 
                        - SOCK_STREAM : 流式协议默认使用 TCP 
                        - SOCK_DGRAM : 报式协议默认使用 UDP 
                - 返回值: 
                        - 成功:返回文件描述符,操作的就是内核缓冲区。 
                        - 失败:-1

Note: The above type and protocol cannot be combined at will. For example, SOCK_STREAM cannot be combined with IPPROTO_UDP. When protocol is 0, the default protocol corresponding to the type type is automatically selected.

When we call socket to create a socket, the returned socket descriptor exists in the protocol family (address family, AF_XXX) space, but does not have a specific address. If you want to assign an address to it, you must call the bind() function, otherwise the system will automatically assign a port randomly when calling connect() or listen().

2. bind() function

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命 名 
        - 功能:绑定,将fd 和本地的IP + 端口进行绑定 
        - 参数: 
                - sockfd : 通过socket函数得到的文件描述符 
                - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息 
                - addrlen : 第二个参数结构体占的内存大小 

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

3. listen() function

int listen(int sockfd, int backlog);      // /proc/sys/net/core/somaxconn 
        - 功能:监听这个socket上的连接 
        - 参数: 
                - sockfd : 通过socket()函数得到的文件描述符 
                - backlog : 未连接的和已经连接的和的最大值, 5 

        As the server needs to monitor whether there is data from the client at all times, the server calls listen() to listen to the established socket. If the client calls connect() to issue a connection request, the server will receive the request.

4. connect() function

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
        - 功能: 客户端连接服务器 
        - 参数: 
                - sockfd : 用于通信的文件描述符 
                - addr : 客户端要连接的服务器的地址信息 
                - addrlen : 第二个参数的内存大小 
        - 返回值:成功 0, 失败 -1 

 The client establishes a connection with the TCP server by calling the connect function

5. accept() function

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 
        - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 
        - 参数: 
                - sockfd : 用于监听的文件描述符 
                - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
                - addrlen : 指定第二个参数的对应的内存大小 
        - 返回值: 
                - 成功 :用于通信的文件描述符 
                - -1 : 失败 

        After calling socket(), bind(), and listen(), the server side will listen to the specified socket address. After calling socket() and connect(), the client establishes a connection channel and sends a request to the server. After the server monitors the request, it calls the accept() function to receive the request. Then you can start network I/O operations, which are similar to ordinary file read and write I/O operations.

6. read(), write() functions

ssize_t write(int fd, const void *buf, size_t count);      // 写数据 
ssize_t read(int fd, void *buf, size_t count);                // 读数据

        The read function is responsible for reading content from fd. When the read is successful, read returns the actual number of bytes read. If the returned value is 0, it means that the end of the file has been read. If it is less than 0, it means that an error has occurred. If the error is EINTR, it means that the read was caused by an interrupt. If it is ECONNREST, it means there is a problem with the network connection.

The write function writes the nbytes bytes in buf to the file descriptor fd. When successful, it returns the number of bytes written. On failure, -1 is returned and the errno variable is set. In network programs, there are two possibilities when we write to the socket file descriptor. 1) The return value of write is greater than 0, indicating that part or all of the data has been written. 2) The returned value is less than 0, and an error occurred. We have to deal with it according to the error type. If the error is EINTR, it means that an interrupt error occurred during writing. If it is EPIPE, it means there is a problem with the network connection (the other party has closed the connection).

        Network I/O operations are not only read()/write() functions, but also the following groups

read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

Its statement is as follows:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
			  const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
				struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

7. close() function

int close(int fd);

        After closing a socket connection, it will immediately return to the calling process. This descriptor can no longer be used by the calling process, that is, it can no longer be used as the first parameter of read or write.

Note: The close operation only reduces the reference count of the corresponding socket descriptor by -1. Only when the reference count is 0 will the TCP client be triggered to send a termination request to the server.

Four: Socket communication practice

client:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>

#define SERVER_PORT 8888
 
int main()
{
 
   //客户端只需要一个套接字文件描述符,用于和服务器通信
   int serverSocket;
    
   //描述服务器的socket
   struct sockaddr_in serverAddr;
    
   char sendbuf[200]; //存储 发送的信息 
   char recvbuf[200]; //存储 接收到的信息 
    
   int iDataNum;
   

  /*********************************************************************/
  /*                          1-创建客户端套接字                        */
  /*********************************************************************/
   if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
   {
      perror("socket");
      return 1;
   }
    
   serverAddr.sin_family = AF_INET;
   serverAddr.sin_port = htons(SERVER_PORT);
    
   //指定服务器端的ip,本地测试:127.0.0.1
   //inet_addr()函数,将点分十进制IP转换成网络字节序IP
   serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
  
  /*********************************************************************/
  /*                          2-连接服务端                              */
  /*********************************************************************/  
   if(connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
   {
      perror("connect");
      return 1;
   }
    
   printf("连接到主机...\n");
    
  /*********************************************************************/
  /*                          3-发送接收消息                            */
  /*********************************************************************/ 
   while(1)
   {
      printf("发送消息:");
      scanf("%s", sendbuf);
      printf("\n");
      send(serverSocket, sendbuf, strlen(sendbuf), 0); //向服务端发送消息
      if(strcmp(sendbuf, "quit") == 0) break;
      printf("读取消息:");
      recvbuf[0] = '\0';
      iDataNum = recv(serverSocket, recvbuf, 200, 0); //接收服务端发来的消息
      recvbuf[iDataNum] = '\0';
      printf("%s\n", recvbuf);
   }

   close(serverSocket);
   
   return 0;
}

Server:

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

#define SERVER_PORT 8888
 
/*
监听后,一直处于accept阻塞状态,
直到有客户端连接,
当客户端如close后,断开与客户端的连接
*/
 
int main()
{
   
   //调用socket函数返回的文件描述符
   int serverSocket;
    
   //声明两个套接字sockaddr_in结构体变量,分别表示客户端和服务器
   struct sockaddr_in server_addr;
   struct sockaddr_in clientAddr;
    
   int addr_len = sizeof(clientAddr);
   int clientSocket;
   char buffer[200]; //存储 发送和接收的信息 
   int iDataNum;

   
  /*********************************************************************/
  /*                          1-创建服务端套接字                        */
  /*********************************************************************/
   if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
   {
      perror("socket");
      return 1;
   }
   memset(&server_addr,0, sizeof(server_addr));
    
   //初始化服务器端的套接字,并用htons和htonl将端口和地址转成网络字节序
   server_addr.sin_family = AF_INET;
   server_addr.sin_port = htons(SERVER_PORT);
    
   //ip可是是本服务器的ip,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址
   server_addr.sin_addr.s_addr = htonl(INADDR_ANY);


 
   //对于bind,accept之类的函数,里面套接字参数都是需要强制转换成(struct sockaddr *)
   //bind三个参数:服务器端的套接字的文件描述符
  /*********************************************************************/
  /*                          2-服务端绑定监听的IP和por                  */
  /*********************************************************************/  
   if(bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
   {
      perror("connect");
      return 1;
   }
    

  /*********************************************************************/
  /*                          3-服务端开始监听                          */
  /*********************************************************************/ 
   if(listen(serverSocket, 5) < 0)//开启监听 ,第二个参数是最大监听数
   {
      perror("listen");
      return 1;
   }
   
  /*********************************************************************/
  /*                          4-接收发送消息                            */
  /*********************************************************************/  
  printf("监听端口: %d\n", SERVER_PORT);
  
  //调用accept函数后,会进入阻塞状态
  //accept返回一个套接字的文件描述符,这样服务器端便有两个套接字的文件描述符,
  //serverSocket和client。
  //serverSocket仍然继续在监听状态,client则负责接收和发送数据
  //clientAddr是一个传出参数,accept返回时,传出客户端的地址和端口号
  //addr_len是一个传入-传出参数,传入的是调用者提供的缓冲区的clientAddr的长度,以避免缓冲区溢出。
  //传出的是客户端地址结构体的实际长度。
  //出错返回-1
  
  clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, (socklen_t*)&addr_len);
  
  if(clientSocket < 0)
  {
     perror("accept");
     
  }
   
  printf("等待消息...\n");
  
  //inet_ntoa ip地址转换函数,将网络字节序IP转换为点分十进制IP
  //表达式:char *inet_ntoa (struct in_addr);
  printf("IP is %s\n", inet_ntoa(clientAddr.sin_addr)); //把来访问的客户端的IP地址打出来
  printf("Port is %d\n", htons(clientAddr.sin_port)); 
   
  while(1)
  {
       buffer[0] = '\0';
       iDataNum = recv(clientSocket, buffer, 1024, 0);
       if(iDataNum < 0)
       {
          continue;
       }
       buffer[iDataNum] = '\0';
       if(strcmp(buffer, "quit") == 0) break;
       printf("收到消息: %s\n", buffer);
       printf("发送消息:");
       scanf("%s", buffer);
       send(clientSocket, buffer, strlen(buffer), 0); //服务端也向客户端发送消息 
       if(strcmp(buffer, "quit") == 0) break; //输入quit停止服务端程序 
  }

   close(clientSocket);
   close(serverSocket);
   
   return 0;
    
}

 Compile with gcc on Linux:

gcc server.c -o server
gcc client.c -o client

Run the server first:

Run the client again:

The client server sends and receives messages:

Guess you like

Origin blog.csdn.net/qq_27071221/article/details/132617811