Socket 【Network Communication - Socket】

1. Brief introduction

Regarding network communication, the iOS network programming level interfaces we are currently exposed to can be divided into the following three layers from shallow to deep:

Cocoa layer : The top layer is also the most advanced, OC-based API, such as URL access, NSStream, Bonjour, GameKit, etc. This is the API we commonly use in most cases. The Cocoa layer is implemented based on Core Foundation. It has strong encapsulation, high development efficiency and low execution efficiency. (But under the basis that the CPU and memory of today's devices are already very powerful, using the interface of the Cocoa layer for network programming will not sacrifice too much execution time, and the efficiency is also very impressive.)

Core Foundation layer : C-based CFNetwork and CFNetServices. Because direct use of sockets requires more programming work, Apple simply encapsulates Sockets at the OS layer to simplify programming tasks. This layer provides CFNetwork and CFNetServices, among which CFNetwork is based on CFStream and CFSocket.

OS layer : C-based BSD Socket. BSD Socket is what we usually call "Berkeley Socket". The bottom-level BSD Socket provides the greatest degree of control over network programming, but also has the most programming work. Therefore, Apple recommends that we use the API of Core Foundation and above for programming.


This article is mainly to analyze the network communication from the bottom Socket.


If it's not clear enough, then see the explanation below:


The picture above is the OSI network seven-layer protocol. Each layer builds on the foundation of the layer below. First of all, we must first understand the TCP/IP protocol.

We know that the IP address of the network layer can uniquely identify the host, and the transport layer protocol and port number can uniquely identify a process of the host, so we can use the IP address + protocol + port number to uniquely identify a process in the network. Then we will use Socket for network communication.

Socket can actually be seen as an abstraction layer between the transport layer and the application layer. He converts the complex TCP/IP protocol into several simple interfaces for the application layer to invoke the communication of the implemented processes in the network. That is to say, Socket is not a protocol, it is actually an encapsulation of the TCP/IP protocol, and Socket is just a calling interface (API). Through Socket we can use the TCP/IP protocol. There is a relatively easy-to-understand sentence: "TCP/IP is just a protocol stack, just like the operating mechanism of an operating system, it must be implemented specifically, and an external operation interface must also be provided."


2. Socket programming API (refer to Linux Socket programming )

(1) socket function

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

The socket function corresponds to the open operation of a normal file. The open operation of a normal file returns a file descriptor, and socket() is used to create a socket descriptor, which uniquely identifies a socket. This socket description word is the same as the file description word. It is used in subsequent operations, and it is used as a parameter to perform some read and write operations.

Just as you can pass different parameter values ​​to fopen to open different files. 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 determines the address type of the socket, and the corresponding address must be used in communication. For example, AF_INET decides to use a combination of ipv4 address (32-bit) and port number (16-bit), AF_UNIX decides to use an absolute path name as address.
  • type: Specifies the socket type. Commonly used socket types are SOCK_STREAM , SOCK_DGRAM , SOCK_RAW , SOCK_PACKET , SOCK_SEQPACKET and so on (what are the types of sockets?).
  • Protocol: As the name implies, it is to specify the protocol. Commonly used protocols are IPPROTO_TCP , IPPTTOO_UDP , IPPROTO_SCTP , IPPROTO_TIPC , etc., which correspond to TCP transport protocol, UDP transport protocol, STCP transport protocol, TIPC transport protocol (I will discuss this protocol separately!).

Note: It is not that the above types and protocols can 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 description word 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 random port when you call connect() and listen() .

(2) bind function

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

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

The three parameters of the function are:

  • sockfd: the socket description word, which is created by the socket() function and uniquely identifies a socket. The bind () function is to bind a name to this descriptor.
  • addr: A const struct sockaddr * pointer to the protocol address to be bound to sockfd. This address structure is different according to the address protocol family when the socket is created. 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 */
    };
    
    /* Internet address. */
    struct in_addr {
        uint32_t       s_addr;     /* address in network byte order */
    };
    ipv6 corresponds to: 
    struct sockaddr_in6 {
        sa_family_t sin6_family; / * AF_INET6 */
        in_port_t       sin6_port;     /* port number */
        uint32_t        sin6_flowinfo; /* IPv6 flow information */
        struct in6_addr sin6_addr;     /* IPv6 address */
        uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */
    };
    
    struct in6_addr {
        unsigned char   s6_addr[16];   /* IPv6 address */
    };
    The Unix domain corresponds to: 
    #define UNIX_PATH_MAX    108
    
    struct sockaddr_un {
        sa_family_t sun_family;               /* AF_UNIX */
        char        sun_path[UNIX_PATH_MAX];  /* pathname */
    };
  • addrlen: corresponds to the length of the address.

Usually the server will be bound to a well-known address (such as ip address + port number) when it is started to provide services, and the client can connect to the server through it; and the client does not need to specify, the system automatically assigns a port number and its own ip address. This is why usually the server side will call bind () before listen , but the client will not call it, but one will be randomly generated by the system when connect ().

Network Endianness vs Host Endianness

The host byte order is what we usually call big endian and little endian mode: different CPUs have different byte order types, and these byte orders refer to the order in which integers are stored in memory, which is called host order. The definitions of Big-Endian and Little-Endian citing the standard are as follows:

  a) Little-Endian means that the low-order bytes are placed at the low address end of the memory, and the high-order bytes are placed at the high address end of the memory.

  b) Big-Endian means that the high-order bytes are placed at the low address end of the memory, and the low-order bytes are placed at the high address end of the memory.

Network byte order : 4 bytes of 32-bit values ​​are transmitted in the following order: first 0 to 7 bits, then 8 to 15 bits, then 16 to 23 bits, and finally 24 to 31 bits. This transmission order is called big endian. Since all binary integers in the TCP/IP header are required to be in this order when transmitted over the network, it is also called network byte order. Byte order, as the name implies, is the order in which data of more than one byte type is stored in memory. There is no problem with the order of one byte of data.

So : when binding an address to a socket, please convert the host byte order to the network byte order first, instead of assuming that the host byte order is the same as the network byte order using Big-Endian. Because of this problem has caused bloodshed! Due to this problem in the company's project code, there are many inexplicable problems, so please remember not to make any assumptions about the host byte order, and be sure to convert it to the network byte order and assign it to the socket.


(3) listen and connect functions

As a server, after calling socket() and bind () , it will call listen() to monitor the socket. If the client calls connect() at this time to send a connection request, 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 description of the socket to be monitored, and the second parameter is the maximum number of connections that the corresponding socket can queue. The socket created by the socket() function is an active type by default, and the listen function turns the socket into 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.

(4) accept function

After the TCP server calls socket() , bind () , listen() in sequence , it will listen to the specified socket address. After the TCP client calls socket() and connect() in turn, it sends a connection request to the TCP server. After the TCP server listens to the request, it will call the accept () function to receive the request, so that the connection is established. After that, you can start network I/O operations, that is, read and write I/O operations similar to 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 a pointer to struct  sockaddr *, which is used to return the client's protocol address, and the third parameter is the length of the protocol address. If accpet succeeds, its return value is a brand new descriptor automatically generated by the kernel, representing the TCP connection to the returning client.

Note : The first parameter of accept is the server's socket description, which is generated by the server calling the socket() function, which is called the listening socket description ; and the accept function returns the connected socket description . A server usually only creates a listening socket descriptor, which exists for the lifetime of the server. The kernel creates a connected socket descriptor for each client connection accepted by the server process. When the server completes the service to a client, the corresponding connected socket descriptor is closed.

(5) read and write functions

Everything is in place and only owes Dongfeng, so far the server and the client have established a good connection. You can call the network I/O for read and write operations, that is, to realize the communication between different processes in the network! There are several groups of network I/O operations:

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

I recommend using the recvmsg()/sendmsg() functions, these two functions are the most general I/O functions, and you can actually replace the other functions above with these two functions. They are declared as follows:

       #include <unistd.h>

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

       #include <sys/types.h>
       #include <sys/socket.h>

       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);

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, and less than 0 means an error occurred. If the error is EINTR, the read is caused by an interrupt, and if it is ECONNREST, there is a problem with the network connection.

The write function writes the nbytes bytes in buf to the file descriptor fd. Returns the number of bytes written on success. Returns -1 on failure and sets the errno variable. In a network program, there are two possibilities when we write to a socket file descriptor.     

    01 - The return value of write is greater than 0, indicating that some or all of the data was written.

    02 - The returned value is less than 0, an error occurred. We want to handle according to the type of error. If the error is EINTR, an interrupt error occurred while writing. If it is EPIPE, it means that there is a problem with the network connection (the other party has closed the connection).

I will not introduce these pairs of I/O functions one by one. For details, please refer to the man document or baidu and Google. In the following example, send/recv will be used.

(6) close function

After the connection is established between the server and the client, some read and write operations will be performed. After the read and write operations are completed, the corresponding socket descriptor should be closed.

#include <unistd.h>
int close(int fd);

The default behavior of close a TCP socket is to mark the socket as closed, and then return immediately to the calling process. The 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 makes the reference count of the corresponding socket description word -1. Only when the reference count is 0 will the TCP client be triggered to send a connection termination request to the server.


3. Example reference

     ViewController.m  
//  Socket  
//  
//  Created by CoderZYWang on 2017/3/1.  
//  Copyright © 2017年 CoderZYWang_mac. All rights reserved.  
//  
  
/**
 Client socket network request process
 (1) The client calls socket(...) to create a socket;
 (2) The client calls connect(...) to initiate a connection request to the server to establish a connection;
 (3) After the client establishes a connection with the server, it can send or receive data to or from the client through send(...)/receive(...);
 (4) The client calls close to close the socket;
 */  
  
#import "ViewController.h"  
  
#import <arpa/inet.h> // Provide IP address conversion function  
#import <netdb.h> // Provides functions for setting and obtaining domain names  
  
@interface ViewController ()  
  
@end  
  
@implementation ViewController  
  
- (void)viewDidLoad {  
    [super viewDidLoad];  
    // Do any additional setup after loading the view, typically from a nib.  
      
    // create thread  
    [self createThread];  
}  
  
/** create thread */  
- (void)createThread {  
    // Interfaces such as connect/recv/send are blocking, so we need to put these operations on non-UI threads  
    dispatch_async(dispatch_get_global_queue(0, 0), ^{  
        [self loadDataWithUrl:[NSURL URLWithString:@"domain name"]];  
        // Default port number http 80 http 443  
    });  
}  
  
/** request data */  
- (void)loadDataWithUrl:(NSURL *)url {  
    NSString *host = [url host]; // Get the url host address  
    NSNumber *port = [url port]; // Get the url port number  
      
// int socket = socket(AF_INET6, SOCK_STREAM, 0); cannot be named with socket, the keyword will report an error  
    // Create and initialize socket, return the descriptor of the socket file, if it is -1, it means that the socket creation failed  
    int socketFileDescriptor = socket(AF_INET, SOCK_STREAM, 0); // This function is based on #import <arpa/inet.h>  
      
    if (socketFileDescriptor == -1) {  
        NSLog(@"socket creation failed");  
        return;  
    }  
      
    // Get IP address related information from the host  
    struct hostent *remoteHostEnt = gethostbyname([host UTF8String]);  
      
    if (remoteHostEnt == NULL) {  
        close(socketFileDescriptor);  
        NSLog(@"Unable to resolve the hostname of the repository server");  
        return;  
    }  
      
    // Get the server address list from the host address structure (h_addr_list is a parameter in the hostent structure)  
    struct in_addr *remoteInAddr = (struct in_addr *)remoteHostEnt -> h_addr_list[0];  
      
    // set socket parameters  
    struct sockaddr_in socketParameters;  
    socketParameters.sin_family = AF_INET; // sin_family refers to the protocol family  
    socketParameters.sin_addr = *remoteInAddr; // h_addr_list[0] is a secondary pointer. sin_addr stores IP addresses, using the in_addr data structure  
    socketParameters.sin_port = htons([port intValue]); // sin_port stores the port number (in network byte order)  
      
    // The client sends a connection request to the server at a specific network address, and returns 0 if the connection is successful, and -1 if it fails.  
    // When the server is established, the client initiates a connection establishment request to the server by calling this interface.  
    // For UDP, this interface is optional. If this interface is called, it means that the default network address of the UDP socket is set.  
    // For TCP sockets this is where the legendary three-way handshake establishes the connection.  
    // Note: This interface call will block the current thread until the server returns  
    // connect(<#int#> unconnected socket, <#const struct sockaddr *#> socket parameter pointer, <#socklen_t#> socket parameter length)  
    int ret = connect(socketFileDescriptor, (struct sockaddr *)&socketParameters, sizeof(socketParameters));  
    if (ret == -1) {  
        close(socketFileDescriptor);  
        NSLog(@"socket connection failed");  
        return;  
    }  
      
    // If the program comes here, the socket has been successfully connected to the server  
      
    // keep getting data from the server until the end of the data  
    NSMutableData * dataM = [NSMutableData data];  
      
    int maxCount = 6; // number of traversals (test)  
    int i = 0;  
    BOOL isNeedWaitReading = YES; // Do you need to continue reading data  
    while (isNeedWaitReading && i < maxCount) {  
        const charchar *buffer[1024];  
        int length = sizeof(buffer);  
          
        // recv(<#int#> The receiver socket descriptor is actually the socket, <#void *#> is the buffer used to store the data received by the recv function, <#size_t#> indicates the length of the buff, < #int#> Usually 0)  
        // Note that the recv function only copies data, the real receiving data is done by the protocol  
          
        // The amount of data to read the buffer from the socket, return the number of bytes read  
        long result = recv(socketFileDescriptor, &buffer, length, 0);  
          
        if (result > 0) {  
            [dataM appendBytes:buffer length:result];  
        } else { // stop traversing if you don't get any data  
            isNeedWaitReading = NO;  
        }  
        ++ i;  
    }  
      
//    const char * buffer[1024];  
//    int length = sizeof(buffer);  
//    long result = recv(socketFileDescriptor, &buffer, length, 0);  
//    [dataM appendBytes:buffer length:result];  
  
    close(socketFileDescriptor);  
      
    [self showData: dataM];  
}  
  
- (void)showData:(NSData *)data {  
    // The main thread gets the data for display  
    dispatch_async(dispatch_get_main_queue(), ^{  
        NSLog(@"data --- %@", data);  
          
        NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];  
//        NSString *dataStr = [NSString stringdata];  
        NSLog(@"dataStr --- %@", dataStr);  
    });  
}  
  
- (void)didReceiveMemoryWarning {  
    [super didReceiveMemoryWarning];  
    // Dispose of any resources that can be recreated.  
}  
  
@end  


Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326802064&siteId=291194637