1. An overview of the

I haven’t written my thoughts recently, but I will simply write a technical article in line with the saying that I will become rusty if I don’t write for a long time. This article mainly discusses the network knowledge, covering a wide range, but not deep A. OK, no more words, the following is mainly to introduce the working principle of the network, and then carry out specific implementation analysis from the language level. Hope to help you.

2. Network status

The Internet is inseparable from the Internet (nonsense). Such as IM, games, web browser and other scenarios, all require network support. The network depends on the protocol, without which network transmission cannot be carried out. Because people don’t know what kind of monster you’re sending.

The most popular protocol is IP. The transport layer is typically TCP and UDP. The game scenario would probably use UDP more, since TCP is a big compromise for reliable data transmission and network links. And that’s as it should be. Multiple protocols exist because of multiple scenarios. If one TCP could solve all problems, it would be enough. The application layer has a generic HTTP protocol.

3. Network working principle

Let’s talk today about how the web connects to work using the use case of a browser accessing a web page. A browser (a piece of software) works through multiple processes. One of these is the process used for web traffic (it’s not clear, but it’s something like that), which starts when we type in a web address and click on it.

Generating HTTP requests

For the request, we should be familiar with the network communication, all need a corresponding protocol request. Browsers use the HTTP protocol at the application layer. Why HTTP, why you need a protocol. One is to allow multiple languages to communicate (e.g., browsers are usually C++, servers can be JAVA or other). The other part is to have some records as required. That is, the public part of each packet.

For example, the first line of an HTTP request contains the following data. Methods can be used to determine what requests are made, urIs can be used to distinguish the resource paths of requests, and so on.

Method space URI Space HTTP versionCopy the code

Of course, we can also implement our own application layer protocol to meet our scenario. While HTTP can be used for any scenario, some excess data can affect network communication performance. Most RPC calls today are custom protocols. This is also true of some game scenarios. For application layer development, the only thing we can probably do is change the application layer protocol.

Maybe some people say you can modify a transport layer protocol based on UDP. But it really depends on UDP (like KCP).

The protocol in question is the application layer. It can be said that each layer of protocol is processed at the corresponding layer.

  • If TCP is the transport layer protocol. After receiving the packet, the TCP module of the operating system performs logical processing in the module, cuts off the TCP header (TCP protocol part), and throws the data to the application layer.
  • If the application layer protocol, our application layer server receives the packet, processes it, and finally encapsulates all the data and throws it back to us for logic.

A packet, after we want to send it, it wraps it up in layers. It is then converted into electrical signals and transmitted to the peer end. The other end will unpack it again. It feels like a stack in and out process.

Querying and Resolving domain names

First, the browser process will obtain the IP address of the domain name according to the entered domain name. This process is called domain name resolution (DNS resolution). Get the IP of the domain name from the local configuration, cache or DNS server. The DNS server also supports domain name query by IP address. This is a reversible process. Detailed we can go to understand the domain name resolution process. We know the purpose and the outcome so far.

TCP modules

Construct application layer request packet, and query the destination. The browser process delegates the TCP module to send the packet. The protocol stack described here describes the TCP/IP module of the operating system. Since it is a module of the operating system, we generally cannot modify its logic directly. The best we can do is optimize transport through the configuration of parameters provided by the operating system. This reader can learn more about it.

Image from How the Internet Is Connected

When TCP is used to send data packets, a connection needs to be established. It takes three handshakes to send and receive data, and four waves after that. The TCP module encapsulates a TCP header for application-layer data. Then throw it to the IP module. The same is true of how TCP establishes connections. This is equivalent to sending a TCP header with no application-layer data and marking the response field to tell the peer that this is a request packet to establish a connection.

Specific TCP module content, we can go to see the kernel TCP source code. The process is quite complicated (after all, there are various issues to consider in industrial landing), but the general understanding should be ok. Or look at the related paper.

IP module

IP module is also responsible for more things. Because after the IP module processing, the network card only need to transmit the packet. The IP module adds the IP header and MAC header information. The IP header mainly contains the control information for sending the packet to the destination.

The IP module does not care about data from the upper-layer protocol. The IP module treats UDP and TCP packets as a single piece of binary data. He was only responsible for sending out the data. It doesn’t care if the package is in order or lost. This is also in line with traditional design principles. The module layers should be decoupled from each other.

The IP header

The main information in the IP header is the destination address and source address of the packet. The destination address is informed by the upper layer, namely the TCP module (TCP gets from the application layer). The source address is obtained based on the destination address. Why do you say that? Packets are actually sent through the network card. Each network adapter corresponds to an IP address. We need to query the network adapter from which the data packet should be sent according to the destination address.

This query relies on the IP routing table.

Leave aside the rest of the IP header data.

The MAC header

Each network card has a MAC address. It is written into the ROM of the network card at production time. If we know the nic, we know the source MAC address. The destination MAC address is queried by the gateway through THE ARP protocol. You can learn the ARP implementation principle by yourself.

You may wonder why the IP module is responsible for blocking MAC addresses. After all, Ethernet is supposed to be responsible for the MAC header. In fact is to reduce the network card work. After all, the IP module can do the job. If another protocol replaces the IP protocol later. Network card module such design can also be directly supported.

After successful encapsulation, the data will be converted into electrical signals and sent out depending on the network adapter. The specific sending process will not be said, involving the physical layer, the details can be understood.

Today’s network traffic relies on routers. And routers today have inherited the functions of switches and hubs. So you can directly understand how the router works.

IP fragmentation

One thing to note here is that routers are implemented over IP. So the ROUTER’s IP module is similar to that of the computer above. After receiving the packet, the router determines the next hop of the packet based on the IP address. And packages may be sharded depending on some restrictions. IP sharding. Sharding depends on the configuration of the packet (whether it is allowed or not) and the MTU that the destination router can accept. Of course, IP sharding can only degrade transmission performance. Therefore, we usually should avoid.

We cannot only ensure that the application packet is smaller than the MTU. This MTU also includes TCP and IP headers. So we have to subtract these two things. This is especially true for scenarios that require performance. We need to focus.

Image from How the Internet Is Connected

Receiving network packets

Actually receiving is just the reverse of sending.

The network adapter converts the electrical signal into a digital signal and sends it to the MAC module. The MAC module mainly checks whether the packet is sent to itself and whether any errors occur. Otherwise, packets are discarded. When this is done, the packet is put into a buffer and the operating system is notified, by means of an interrupt.

The CPU receives the interrupt signal and switches to the interrupt handler. The interrupt handler invokes the nic driver, reads the data from the NIC buffer, and sends it to the corresponding module according to the protocol of the data. Usually IP modules.

And then it goes from IP to TCP to the application server. Finally, it passes to the business layer.

4. Source code analysis

Next, I’ll focus on code analysis of how we communicate on the network. It’s a lot easier to understand in C code, but I mostly do JAVA, so let’s focus on how JAVA encapsulates things.

At the code level, network communication relies on sockets. Here we go directly to the socket of the protocol stack (in this case the transport layer). The socket of the protocol stack is actually a piece of memory space of the protocol stack. Some control information needed for communication is stored structurally. Such as IP and port number, communication status and so on. The protocol stack relies on this memory space for data transfer.

The same is true of sockets in JAVA, except that they do not correspond directly to sockets in the protocol stack. I’m abstracting it out. Finally, the pair is connected to the protocol stack. This layer of abstraction ensures that some of our operations in Java map directly to the underlying protocol. (Java is a higher level of abstraction.)

OK, understand socket. We can go into the source code!

Server:
ServerSocket ss =new ServerSocket(10000);
Socket s = ss.accept();
Client:
Socket s = new Socket("localhost".10000);
Copy the code

Here is a simple example. The server initializes a ServerSocket. And call the Accept method to listen for client connections. When the client initializes the Socket, it connects to the server.

ServerSocket construction process

try {
    SecurityManager security = System.getSecurityManager();
    if(security ! =null)
        security.checkListen(epoint.getPort());
    getImpl().bind(epoint.getAddress(), epoint.getPort());
    getImpl().listen(backlog);
    bound = true;
}
Copy the code

When the ServerSocket object is initialized, the SocketImpl is created. The bind method is then called, followed by the LISTEN method.

The logic is simple. It’s not the ServerSocket that really communicates with the underlying layer, it’s the SocketImpl.

GetImpl method

SocketImpl getImpl(a) throws SocketException {
    if(! created) createImpl();return impl;
}
void createImpl(a) throws SocketException {
    if (impl == null)
        setImpl();
    try {
        impl.create(true);
        created = true;
    } catch (IOException e) {
        throw newSocketException(e.getMessage()); }}Copy the code

Create sokcet if SocketImpl is empty, otherwise call the imp.create method to create sokcet.

fd = new FileDescriptor();
socketCreate(true);
Copy the code

Server socket creation logic so two lines of code.

1. Create a FileDescriptor. Used to record the file descriptor corresponding to the underlying socket. After the underlying socket is created, the fd of the socket is returned. After the underlying JNI creates the socket, it assigns the returned FD value to the FileDescriptor of the new descriptor. The specific implementation is to directly change the memory address of the object fd assignment. You can do your own research on the details.

2. The true parameter indicates serverSocket.

SocketCreate method

if ((fd = socket(domain, type, 0)) = =- 1) {
    /* note: if you run out of fds, you may not be able to load * the exception class, and get a NoClassDefFoundError * instead. */
    NET_ThrowNew(env, errno, "can't create socket");
    return; }...if (isServer) {
    int arg = 1;
    SET_NONBLOCKING(fd);
    if (NET_SetSockOpt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&arg,
                   sizeof(arg)) < 0) {
        NET_ThrowNew(env, errno, "cannot set SO_REUSEADDR");
        close(fd);
        return;
    }
}
(*env)->SetIntField(env, fdObj, IO_fd_fdID, fd);
Copy the code

Part of the core code is captured above.

The socket method is first called to create a TCP socket. Domain is the IP version. The Linux socket function returns the corresponding file descriptor. The server socket is set to non-blocking.

So the SetIntField method is what I said above to assign to the FileDescriptor that we’re creating. FdObj is the FileDescriptor object that we created. Few details.

The bind method

protected synchronized void bind(InetAddress address, int lport)
    throws IOException
{
   synchronized (fdLock) {
        if(! closePending && (socket ==null| |! socket.isBound())) { NetHooks.beforeTcpBind(fd, address, lport); } } socketBind(address, lport);if(socket ! =null)
        socket.setBound();
    if(serverSocket ! =null)
        serverSocket.setBound();
}
Copy the code

Based on the created InetAddress and local port, the underlying socketBind method is called.

/* bind */
if(NET_InetAddressToSockaddr(env, iaObj, localport, &sa, &len, JNI_TRUE) ! =0) {
    return;
}

if (NET_Bind(fd, &sa, len) < 0) {
    if(errno == EADDRINUSE || errno == EADDRNOTAVAIL || errno == EPERM || errno == EACCES) { NET_ThrowByNameWithLastError(env,  JNU_JAVANETPKG"BindException"."Bind failed");
    } else {
        JNU_ThrowByNameWithMessageAndLastError
            (env, JNU_JAVANETPKG "SocketException"."Bind failed");
    }
    return;
}
Copy the code

The underlying binding method is partially coded as above.

1.NET_InetAddressToSockaddr, the InetAddress data and port of the Java layer are passed into the SA. This is essentially converting a Java object into a C structure.

2. Call NET_Bind to bind.

3. After success, set localPort and address for the Java layer. Of course, if we pass port 0, the OS will generate a port for us.

Listen method

This method is simple, calling the underlying listen directly.

if (listen(fd, count) == - 1) {
    JNU_ThrowByNameWithMessageAndLastError
        (env, JNU_JAVANETPKG "SocketException"."Listen failed");
}
Copy the code

The count parameter represents the maximum number of connections that can be processed simultaneously. At this stage the socket creation is complete. Note that calling Listen does not mean that the socket can accept connections. Accept is.

Accept method

public Socket accept(a) throws IOException {
    if (isClosed())
        throw new SocketException("Socket is closed");
    if(! isBound())throw new SocketException("Socket is not bound yet");
    Socket s = new Socket((SocketImpl) null);
    implAccept(s);
    return s;
}
Copy the code

The implAccept method simply creates a SocketImpl for the Socket.

What to make of the above statement?

In fact, each client establishes a connection with the server. The server will copy a socket (in this case the protocol layer), and as described above, the underlying socket is mapped to the Java layer SocketImpl. The Java layer socket encapsulates the SocketImpl. So to return a valid Java layer socket, we need to fill in the SocketImpl. That is, call the accept method below.

// The purpose of this method is to populate the socket's SocketImpl object.
protected void accept(SocketImpl s) throws IOException {
    acquireFD();
    try {
        socketAccept(s);
    } finally{ releaseFD(); }}Copy the code

AcquireFD is basically a reference count to a FD. When finished, releaseFD needs to be called. So we have to be careful here.

SocketAccept method

for (;;) {
    int ret;
    jlong currNanoTime;
    /* omit */
    newfd = NET_Accept(fd, &sa.sa, &slen);

    /* connection accepted */
    if (newfd >= 0) {
        SET_BLOCKING(newfd);
        break;
    }

    /* non (ECONNABORTED or EWOULDBLOCK or EAGAIN) error */
    if(! (errno == ECONNABORTED || errno == EWOULDBLOCK || errno == EAGAIN)) {break;
    }

    /* ECONNABORTED or EWOULDBLOCK or EAGAIN error so adjust timeout if there is one. */
    if (nanoTimeout >= NET_NSEC_PER_MSEC) {
        currNanoTime = JVM_NanoTime(env, 0);
        nanoTimeout -= (currNanoTime - prevNanoTime);
        if (nanoTimeout < NET_NSEC_PER_MSEC) {
            JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException"."Accept timed out");
            return;
        }
        prevNanoTime = currNanoTime;
    }
socketFdObj = (*env)->GetObjectField(env, socket, psi_fdID);
(*env)->SetIntField(env, socketFdObj, IO_fd_fdID, newfd);


(*env)->SetObjectField(env, socket, psi_addressID, socketAddressObj);
(*env)->SetIntField(env, socket, psi_portID, port);
/* also fill up the local port information */
 port = (*env)->GetIntField(env, this, psi_localportID);
(*env)->SetIntField(env, socket, psi_localportID, port);
Copy the code

The above code is omitted for simplicity. The main logic is as follows:

1. Call the NET_Accept method to obtain the connection request.

2. If the connection is valid, set it to block and break. Non-econnaborted or EWOULDBLOCK or EAGAIN errors break directly

3. If a timeout occurs, return.

4. Outside the loop, assign the SocketImpl value if the fd is valid, that is, if the new connection is valid. This includes remote ports, local ports, and Addresses.

It is important to note that accept only returns the fd of a new socket. So how does this new socket come about?

In fact, the new socket is copied from the listening socket. So for every new connection. The server will have a socket corresponding to it. The local port and address of the socket are the same as those of the listening socket. It just adds the address and port of the remote socket. This way, if data arrives, TCP can determine which socket buffer to throw the data to.

From the above source can be seen. At the Java level, the network is all about calling C. C to create the TCP socket through the system call. And returns the created information. We don’t need to worry about the creation process. But the fundamentals should be clear.

5. Say more

The above source code is just a simple analysis of the principle of socket, do Java people, is certainly not directly using socket programming. There are some excellent frameworks in Java for networking. For example, netty. It is based on JDK1.4 NIO. Implement a high-performance IO model. In Linux servers, the epoll model can be used for efficient network communication.

No matter what network model you use, it depends on the business. A lot of people talk a little too much about epoll. To put it bluntly, Epoll is just a patch for the OS. It wouldn’t have happened if the OS had been designed with high concurrency in mind. After all, frequent calls to epoll_ctl can have an impact on performance. An inappropriate example: If the network had no latency, would you still use epoll?

Of course, epoll is still valid for the time being. I’ll explain in more detail how epoll works to ensure high performance later.

6. Summary

This paper gives a simple description of network programming. Just to give you a sense of how networks work. Although we don’t use this in our daily development, the framework we use is encapsulated in this aspect. The framework is simply a better use of the base library, optimized as much as possible. It is easy to write efficient programs faster.