[Linux] Network socket programming

foreword

        After mastering a certain network foundation, we can start with the code, use the UDP protocol/TCP protocol to write the socket program, and understand how the server and the client in the network are connected and communicated.

Table of contents

1. Understand the source and destination IP, port, network byte order, socket

The port number:

socket:

Know the TCP/UDP protocol on the transport layer:

Network byte order:

Common sockets:

Two, UDP network programming

1.0 The client sends a message to the server, and the server returns the message to the corresponding client.

socket

bind

Local byte order and network byte order are converted to each other

recvfrom&recv&recvmsg

send&sendto&sendmsg

client:

127.0.0.1 local loopback address

 2.0 The windows client sends a message to the Linux server, and the server returns the message to the corresponding client.

3. TCP network programming

1.0 The client sends a message to the server, and the server returns the message to the corresponding client.

listen

accept

connect


1. Understand the source and destination IP, port, network byte order, socket

        First of all, we know that two hosts communicate on the network, so a source IP and a destination IP must be needed . According to this IP (the IP here is considered as the public network IP, and its uniqueness is guaranteed in a specific area), a uniqueness in the entire network can be determined.

        To communicate between the following hosts, let's understand what the purpose of the communication is:

         Normally, is sending data to the other party's machine the purpose? It can be found that the machine is only used as a tool, and it is the software on the machine that communicates. So the essence of the real network communication process is inter-process communication ! Forwarding data between hosts is just a means. After the machine is received, it is delivered to the designated process!

        Because the process is involved, when one of the hosts receives the network information, how can it execute the corresponding network process stored in the memory by unpacking it? This is related to the port number.

The port number:

        The content of the transport layer protocol. is the uniqueness that identifies a network process on a particular host!

        Why is the uniqueness of the network process? The first is the uniqueness of the host IP in the entire network + the uniqueness of the process in this host. So this combination is the only process in the whole network.

        And the port number and the process id are distinguished, decoupling - the process id is the one that manages the system, and the port number is the one that manages the network. For a process, a process can bind multiple port numbers (that is, there are different IPs to communicate with its process), but the port number can only be used for one process. (Otherwise the uniqueness will not be determined)

        Of course, since IP has a source IP and a destination IP, there is also a source port number and a destination port number for the port number, indicating who sent it and who will receive it.

socket:

        In fact, we combine the source IP and source port number together, and the destination IP and destination port number together, which is the socket .

        Socket = {IP: port number};

        The port number is 16 bits.

Know the TCP/UDP protocol on the transport layer:

UDP: User Datagram Protocol
connection: no connection
Reliable: unreliable transmission (there are problems such as packet loss)

Datagram Oriented

TCP: Transmission Control Protocol
Connection: connected
Reliable: reliable transmission
stream-oriented

        Reliable refers to a neutral description. There is a packet loss problem, and it is unreliable to tolerate in some scenarios. Reliability is a lot of coding and processing. UDP just sends the data, which is simpler. ---When choosing a protocol, according to the needs of the scene: for example, live broadcast and video websites are suitable for using the UDP protocol.

Network byte order:

        We know that when saving data in computer memory, there is a difference between the high and low bits and the high and low bits of the address - that is, the big and small endian byte order. Because in the process of network communication, we don't know whether the two hosts in communication have the same endianness. If the endianness of storage is opposite, then another host will make an error when reading data.

        Therefore, the network stipulates that all transmissions from the local to the network must be in big endian order . In this case, whether it is a machine with little-endian storage or a big-endian storage machine, the data obtained from the network must be in big-endian byte order every time, so that it can be easily converted and read without problems.

Common sockets:

1. Inter-domain socket named pipe - similar to
2. Original socket to write a lot of tools - spare many upper layer protocols to use the bottom layer.

3. Network socket  

        Theoretically, there are three application scenarios, corresponding to three sets of interfaces. But Linux does not want to design too many interfaces, so all interfaces are unified.

        And in order to manage the content in the socket, the sockaaddr structure is defined, a general (all related to the unified type of socket interface), and another structure for different sockets, which is convenient for mutual conversion and unified interface use.

 sockaddr structure:
    network socket: flag type 16-bit port 32IP address _in AF_INET // PF_INET
    inter-domain socket: flag type 108byte path name _un AF_UNIX
    common: first two bytes: flag type sockaddr
    ....

Two, UDP network programming

         After getting acquainted with the above preparatory content for the first time, I will introduce the interface of UDP protocol network programming through the process of writing code, and can actually make the process of simple chat communication between the client and the server:

1.0 The client sends a message to the server, and the server returns the message to the corresponding client .

server side :

        We encapsulate the server side of the server as a .hpp file. I imagine that the server can be initialized and started in two steps.

UDPServer.hpp

         First determine the member attributes. For socket programming, the first indispensable attribute is socket, which is a socket, which is actually a file descriptor (fd), and the type is int. The second is ip and port. Note that ip is a 16-bit integer and ip is 32-bit. However, ip is usually expressed in dotted decimal , and each part of the number separated by . is 1 byte. If it is unsigned, the representation is 0~255.

// 服务器封装
class UDPServer
{
    //......
private:
    // 源套接字 = 源ip + 源端口
    std::string _SRCip;
    uint16_t _SRCport;
    // UDP对应一套读写套接字对应文件描述符
    int _socket;
};

        Then there is the constructor, which needs to initialize these properties. Why does the following code set the default parameter of ip to be empty? It will be explained in the running code below.

class UDPServer
{
public:
    UDPServer(uint16_t port, std::string ip = "")
        : _SRCip(ip), _SRCport(port), _socket(-1)
    {
    }
//......
};

         Then there is initialization. For the UDP protocol server, first we need to bind the socket we created to the local incoming ip and port number for initialization. At this time, network interface calls are involved. Here we introduce them one by one:

        First create the socket:

socket

header file :

        #include <sys/types.h> 

        #include <sys/socket.h>

Function prototype :

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

Function introduction :

        socket() creates an endpoint for communication and returns a descriptor. (Socket() creates an endpoint for communication and returns a descriptor.)

        domain: This parameter specifies the communication domain; this selects the protocol family for communication. (IPV4 - AF_INET (IPV6 is followed by a 6))

        type: The specified type, which specifies the communication semantics. UDP is SOCK_DGRAM - Datagram TCP is SOCK_STREAM stream.

        protocol: The protocol specifies the specific protocol to be used by the socket. In fact, it can be automatically derived by selecting the first two parameters, and it can be set to 0.

        Return Value: On success, the file descriptor for the new socket is returned. If an error occurs, -1 is returned, and errno is set appropriately.

        After creating the socket, we need to bind the local IP and port, using interface bind:

bind

header file :

        #include <sys/socket.h>

Function prototype :

        int bind(int socket, const struct sockaddr *address, socklen_t address_len);

Function introduction :

        socket: socket file descriptor.

        address: socket structure:

               For the network, you need to use the structure struct sockaddr_in, and then force the struct sockaddr when passing parameters.

                The content attributes contained in struct sockaddr_in are as follows:

                sin_family - protocol family sin_port - port sin_addr.s_addr - ip

                For ip, the server is generally set to 0.0.0.0 or 0, which means that this server can receive any ip (because a server may have multiple network cards, so that the server can obtain data from any IP during work). It is also recommended to bind like this OK, just set the macro INADDR_ANY.

                It should be noted that the protocol family is consistent with the specified communication domain above. In addition, because port and s_addr are to be sent to the network, it is necessary to convert the local byte order to the network byte order . In addition , before initialization, it needs to be cleared . You can use the function memset or bzero to clear it.

        address_len: The size of the socket structure.

        Return Value: Upon successful completion, bind() will return 0; otherwise, it will return -1 and set errno to indicate an error.

        Because when initializing the socket structure, it is necessary to convert the attributes into network byte order, and the following interface provides options:

Local byte order and network byte order are converted to each other

Integer conversion :

        #include <arpa/inet.h>

       uint32_t htonl(uint32_t hostlong); // Convert the unsigned integer (32 bytes) hostlong from host byte order to network byte order.

       uint16_t htons(uint16_t hostshort); //Convert unsigned short integer (16 bytes) hostshort from host byte order to network byte order.

       uint32_t ntohl(uint32_t netlong); // Convert unsigned integer (32 bytes) netlong from network byte order to host byte order.

       uint16_t ntohs(uint16_t netshort); //Convert unsigned short (16 bytes) netshort from network byte order to host byte order.

The above interfaces are generally used in port conversion. For ip, since it is usually represented by a string, there are also corresponding interfaces for converting it into numbers and network byte order.

       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>

       int inet_aton(const char *cp, struct in_addr *inp);

// Convert the Internet host address cp from IPv4's dot notation to binary form (in network byte order) and store it in the structure pointed to by inp. inet_aton() returns nonzero if the address is valid, zero otherwise.

       in_addr_t inet_addr(const char *cp);

//Convert the Internet host address cp from IPv4 dot notation to binary data in network byte order. If the input is invalid, INADDR_NONE (usually -1) is returned. Using this function is problematic because -1 is a valid address (255.255.255.255). It should be avoided in favor of inet_aton(), inet_pton(3) or getaddrinfo(3), which provide a more concise way of expressing error returns.

       in_addr_t inet_network(const char *cp);

// Convert cp (a string in IPv4 numeric dot notation) to a number in host byte order for use as a network address for the Internet. If successful, the address is returned accordingly. Returns -1 if the input is invalid.

       char *inet_ntoa(struct in_addr in);

// Convert the Internet host address given in network byte order to a string represented by IPv4 dotted decimal notation. The string is returned in a statically allocated buffer, which will be overwritten by those subsequent calls.

       struct in_addr inet_makeaddr(int net, int host);

//It is the inverse function of inet_netof() and inet_lnaof(). It returns Internet host addresses in network byte order, created by network number net and local address hosts, both in host byte order.

       in_addr_t inet_lnaof(struct in_addr in);

//The return is the local network address in the Internet address. The return value is in host byte order.

       in_addr_t inet_netof(struct in_addr in);

//The role of the function is to return the network number part of the Internet address. The return value is in host byte order.

        In this way, using the above interface, we can bind the socket with the server ip and address.

class UDPServer
{
public:
//......
// UDP服务器初始化:创建套接字+绑定
    void initserver()
    {
        // 创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0); // 网络-IPV4  面向数据报 协议-填0即可,会根据前面两个选项进行判断
        if (_socket < 0)
        {
            // 返回-1表示创建套接字失败,致命错误
            logMessage(FATAL, "套接字创建失败-%d:%s", errno, strerror(errno));
            exit(1);
        }

        // 绑定本地进程
        struct sockaddr_in local; // 注意头文件必须包含完 man in_addr
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;                                                      // 协议家族
        local.sin_port = htons(_SRCport);                                                // 注意,此处是要发送到网上的,需要转化为网络字节序,使用接口 hton 本地转网络 s是2字节16位
        local.sin_addr.s_addr = _SRCip.empty() ? INADDR_ANY : inet_addr(_SRCip.c_str()); // 如果为空,设置默认ip,此时可以接收任意ip发送的消息,不局限于一个ip。
        // 上述套接字结构初始化完毕,现在进行绑定
        if (bind(_socket, (struct sockaddr *)&local, sizeof local) < 0)
        {
            // 小于零绑定失败!
            logMessage(FATAL, "绑定失败-%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "UDP服务器初始化成功..... %s", strerror(errno));
        // UDP无连接 -初始化完成-
    }
//......
};

        Note that the above log (logMessage) is written using the log.h file in my previous blog, which can be replaced by print or cout.

        After the initialization is complete, we need to write the server startup function. Since the UDP protocol does not require connection and uses datagram transmission, the following interface can be used to send and receive target host information:

recvfrom&recv&recvmsg

header file :

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

Function prototype :

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

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

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

Function introduction :

        recvfrom() and recvmsg() are used to receive messages from a socket, regardless of whether the socket is connection-oriented, they can be used to receive data. Usually used for UDP protocol socket programming.

        The recv() call is normally only used on connected sockets (see connect(2)), and it is the same as recvfrom() with a NULL src_addr argument. Usually used for TCP protocol socket programming.

        sockfd is the corresponding socket file descriptor, buf is the buffer where the received information is stored, len is the size of the buffer, flags is the way of reading, generally set to 0 to block reading. src_addr is the socket structure of the host that transmits information, addrlen is an input and output parameter, the input needs to pass in the size of the original socket structure, and the return is the size of the returned socket structure.

        Return Value: These calls return the number of bytes received, or -1 if an error occurred. If an error occurs, errno is set to indicate the error. When the execution of the peer end is completed, the return value is 0 and the orderly shutdown is performed.

        In particular, for the TCP protocol (recv, read ), if the return value > 0, it means normal reading. When the return value is equal to 0, it means that the peer has closed the connection. When the return value is less than 0, it means the read Failed to fetch, set the error code.

send&sendto&sendmsg

header file :

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

Function prototype :

       ssize_t send(int sockfd, const 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 sendmsg(int sockfd, const struct msghdr *msg, int flags);

Function introduction :

        The send() call should only be used while the socket is connected (so that the intended recipient is known). TCP protocol socket programming use.

        sendto is generally used for UDP protocol socket programming.

        Among them, sockfd is the socket file descriptor, buf is the storage area to be sent, len is the size of the area, flags is usually set to 0, dest_addr is the socket structure to be sent to the target host, addrlen is this structure the size of.

        Return Value: On success, these calls return the number of characters sent. If an error occurs, -1 is returned, and errno is set appropriately.

        The server startup function, first of all, it is clear that the server is a resident process, so it must be an infinite loop, and it will continue to receive messages sent to it by different clients. Type back the sent message when requested here. You can use the received port and ip of the target host to display its content on the server side, and then return the original data.

class UDPServer
{
public:
//......
    // UDP服务器通信开始!
    void start()
    {
        // 正式启动UDP服务器
        // 1.0版本 UDP接收客户端信息,返回给客户端本身消息
        char buffer[1024];
        while (true) // 常驻进程,永远不退出
        {
            // 创建客户端套接字结构,用来接收
            struct sockaddr_in client;
            socklen_t clientlen = sizeof(client);
            bzero(&client, sizeof client); // 清零空间
            // 面向数据报接收消息
            ssize_t n = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &clientlen); // 0阻塞读取
            if (n > 0)
            {
                buffer[n] = '\0';
                // 可以把对应主机的套接字信息提取出来
                std::string clientip = inet_ntoa(client.sin_addr); // 网络 转化ip
                uint16_t clientport = ntohs(client.sin_port);
                printf("[%s: %d]# %s\n", clientip.c_str(), clientport, buffer);
            }
            // 发送对应主机
            sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr *)&client, sizeof(client));
        }
    }
//......
};

        The destructor just needs to release the socket memory, you need to pay attention to judging whether it is initialized, you can use the default value of the socket to judge.

class UDPServer
{
public:
//.......
    ~UDPServer()
    {
        if (_socket != -1)
            close(_socket);
    }
//.......
};

UDPServer.cpp

        Then, we use the command line parameters in the source file to mount and start the server.

#include "UDPServer.hpp"
#include <iostream>
#include <memory>

static void UserManual()
{
    std::cout << "please:./UDPServer port" << std::endl;
}

int main(int arc, char* argv[])
{
    if (arc != 2)
    {
        UserManual();
        exit(-1);
    }
    std::unique_ptr<UDPServer> UDPServer_ptr(new UDPServer(atoi(argv[1])));
    UDPServer_ptr->initserver();
    UDPServer_ptr->start();
    return 0;
}

client:

        The client interface has been introduced above, and will not be repeated below.

        The client, first of all, naturally needs to obtain the server's ip and corresponding port, and then create a socket structure for the server and use the interface sento to send it.

        So we need to think about the question here: Should the client's socket be bound to the local ip and port? Generally, for the client, it is actually the application. If an application binds a specific port each time, based on the network foundation we have learned before, we can know that the port is the only process, so different applications are likely to have the same port bound when writing, then use it at this time There will be problems, that is, one port corresponds to different processes, and the data returned by the server does not know who to send it to.

        Therefore, the client socket does not need to bind the ip and port by itself. When it is sent to the server for the first time, it will be randomly arranged by the operating system, so that the problem of port number conflict can be avoided.

        The client is no longer encapsulated, and the simple code is as follows:

#include "log.hpp"
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>

static void UserManual()
{
    std::cout << "please:./UDPClient ip port" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        UserManual();
        exit(-1);
    }
    // 首先创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    // 注意,此套接字不可显示绑定 - 不能绑定确定的port

    // 将服务器端套接字结构初始化好
    struct sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);  // 首先点分式转化为数字然后转化为网络字节序保存起来

    char buffer[1024];
    while (true)
    {
        std::cout << "请输入# ";
        std::string message;
        std::getline(std::cin, message);
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);

        // 接收消息
        struct sockaddr_in _server;
        socklen_t server_len = sizeof(_server);
        bzero(&_server, server_len);
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&_server, &server_len);
        if (s > 0)
        {
            buffer[s] = '\0';
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    return 0;
}

        We try to run the code under Linux:

127.0.0.1 local loopback address

        First, do a local test. The local test means to bind the server to ip 127.0.0.1 , which is the local loopback address (loopback address), that is, it will not be uploaded to the network, but tested in the local machine. The same local protocol stack will still Run it again, but not through the network interface.

        In this way, the client and server first run locally to troubleshoot the problem, and then they can connect to the network to run, but pay attention to the need to simply modify the processing of the command line parameters written by the server above.

// 在文件UDPServer.cpp 服务器源文件内修改
int main(int arc, char* argv[])
{
    int port;
    std::string ip = "";
    if (arc == 2)
    {
        port = atoi(argv[1]);
    }
    else if (arc == 3)
    {
        port = atoi(argv[2]);
        ip = argv[1];
    }
    else
    {
        UserManual();
        exit(-1);
    }

    std::unique_ptr<UDPServer> UDPServer_ptr(new UDPServer(port, ip));
    UDPServer_ptr->initserver();
    UDPServer_ptr->start();
    return 0;
}

        The test results are as follows:

        The local test is successful, then transfer to the network and try again:

 (The place where the mosaic is played is the public network ip of your own server)

        After testing in the Linux environment, can we communicate with the Windows system?

 2.0 The windows client sends a message to the Linux server, and the server returns the message to the corresponding client .

        In fact, in different operating system environments, although there are some system call variables, they are the same for socket programming and for the network. For the UDP protocol, the Windows client has more initializations than the Linux client (or network programming under Windows) as follows:

#include <WinSock2.h>  // 引入套接字编程库 windows为此库 只需引入一个库就可以了

#pragma warning(disable:4996)  // 屏蔽错误  一般用一些不安全的函数,可以利用此进行屏蔽
#pragma comment(lib, "ws2_32.lib")  // 固定用法,加载入库

int main()
{
	WSADATA WSAData;  // win 初始化
	if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
	{
		//WSACleanup();
		std::cout << "初始化失败!" << std::endl;
		return -1;
	}

    // ......

	closesocket(sock);
	WSACleanup();  // 结束
    return 0;
}

        The rest are basically unchanged.

         It can be seen that the windows side can still send information to the server on the Linux side. However, if the UDP protocol is used for network communication, there will be garbled characters when sending Chinese characters, which needs to be solved by transcoding.

        According to the above two examples of UDP code operation, we can find the problem:

1. First, the socket file descriptor of the UDP protocol can be both written and read, indicating that the socket file descriptor of the UDP protocol is full-duplex.

2. The UDP protocol does not have any connection, as long as the socket related information is created and initialized, the communication is carried out directly.

        Let's take a look at the difference between the network written by the TCP protocol and the UDP protocol, and use some functions on the TCP server side to make our network services more complex and visualized.

3. TCP network programming

1.0 The client sends a message to the server, and the server returns the message to the corresponding client .

        First, let's start with the basics.

TCPServer.hpp

        First of all, the TCP server is still encapsulated to complete the following two functions: 1. Initialization 2. Startup.

        For the encoding of the UDP server, after TCP creates the server socket and binds the server ip and port in the encoding implementation , UDP is initialized, but note that the most important difference between the TCP protocol and the UDP protocol is whether there is a connection. TCP is connected, so it is generally necessary to distinguish the socket type.

        It’s like a restaurant. The owner specially sends a member to call customers on the side of the road. When this member (hereafter referred to as member A) calls the guests, he will hand it over to the waiter in the store, and then member A will continue to call customers. . TCP's socket implementation is actually like this, divided into two sockets, a listening socket and a processing or service socket. The listening socket uses the interface to return the file descriptor of the service socket, which has already bound the socket related information of the client, and because it supports byte stream, it can also use file operation functions such as read and write to operate.

        Then at the time of initialization, you need to set the listening socket to the listening mode at the end , and then the initialization is completed.

listen

header file :

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

Function prototype :

        int listen(int sockfd, int backlog);

Function introduction :

        listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2) (listen() marks the socket referred to by sockfd as a passive socket socket, that is, as the socket that will be used to accept incoming connection requests using accept(2))

        sockfd: socket file descriptor.

        backlog: The backlog parameter defines the maximum length to which sockfd's queue of pending connections may grow. If a connection request arrives when the queue is full, the client may receive an error with an ECONNREFUSED indication, or, if the underlying protocol supports retransmissions, may ignore the request so that a later reattempt to connect succeeds.

        Return Value: On success, zero is returned. Returns -1 on error and sets errno appropriately.

        If it is started, for the UDP protocol, it is a direct communication, because there is relevant information about the client socket structure for receiving such as recvform. But for TCP, at this time, the listening socket first needs to connect to the client connected to the current server in the listening mode, and return a service socket. At this time, for this service socket, the client can The terminal information is extracted or sent. That is to say, a client is connected at this time, and after the listening socket returns, it will continue to listen, and it will continue to return until it is monitored.

accept

header file :

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

Function prototype :

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

Function introduction :

        The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It fetches the first connection request in the pending connection queue to listen to, socket sockfd creates a new connection socket, and returns a new file descriptor referencing that socket. The newly created socket is not in listening state. The original socket is not affected by this call.

        sockfd: listening socket

        addr: output parameter, receiving the socket structure of the client.

        addrlen: Input and output parameters, first input the size of addr, and return the size of the socket structure of the receiving client.

        Return Value: Returns a non-negative integer on correct, -1 and sets errno on error.

        Through the introduction of the above interface, we can basically encapsulate it, because version 1.0 simply returns data from the server to the client, we first implement the following single-process or thread version:

#ifndef _TCPSERVER_
#define _TCPSERVER_

#include "log.hpp"
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

const static int gbacklog = 20; // 一般不能设置太大和太小 -后面再说

static void handleTheClient(int &serviceSock, const std::string &clientIp, uint16_t clientPort)
{
    char buffer[1024];
    while (true)
    {
        printf("[%s: %d]# ", clientIp.c_str(), clientPort);
        // 接收的话注意是字节流,所以以前的文件那一套可以使用的
        ssize_t n = recv(serviceSock, buffer, sizeof(buffer) - 1, 0); // 阻塞等待,没有from就是适合面向字节流的,即TCP的,但是UDP写可以用的
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << buffer << std::endl;

            // 然后发送
            // 可以使用以前文件操作那一套或者send
            // send(serviceSock, buffer, sizeof(buffer) - 1, 0);  // 阻塞发送,使用处理套接字
            ssize_t s = write(serviceSock, buffer, strlen(buffer));
            if (s < 0)
            {
                logMessage(ERROR, "发送信息失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                break;
            }
        }
        else if (n == 0)
        {
            // 对方关闭了连接,这里也进行关闭
            printf("[%s: %d]客户端关闭连接,我也关闭此连接\n");
            close(serviceSock);
            serviceSock = -1; // 防止后面析构再次释放
            break;
        }
        else
        {
            // 小于零说明接收失败
            logMessage(ERROR, "接收信息失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            close(serviceSock);
            break;
        }
    }
}

class TCPServer
{
public:
    TCPServer(uint16_t port, std::string ip = "") : _server_ip(ip), _server_port(port),
                                                    _listen_sock(-1), _service_sock(-1)
    {
    }

    void initTCPServer()
    {
        // 初始化TCP服务器
        _listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 流式套接 自动识别为TCP协议,面向字节流
        if (_listen_sock < 0)
        {
            logMessage(FATAL, "套接字创建失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(1);
        }

        // 服务器将自己的ip和端口绑定到对应的套接字上。
        struct sockaddr_in server_addr;
        memset(&server_addr, 0, sizeof server_addr); // 初始化为0
        server_addr.sin_family = AF_INET;            // 家族还是网络IPV4
        server_addr.sin_port = htons(_server_port);  // 转化为网络字节序的端口号
        if (_server_ip.empty())
            server_addr.sin_addr.s_addr = INADDR_ANY;  // 如果是空字符串就如此处理
        else
        {
            int n = inet_aton(_server_ip.c_str(), &server_addr.sin_addr); // 直接写入结构中,将点分式的ip地址转化为数字然后转化为网络字节序
            if (n == 0)
            {
                logMessage(FATAL, "写入ip地址无效!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                exit(2);
            }
        }

        // bind
        if (bind(_listen_sock, (struct sockaddr *)&server_addr, sizeof server_addr) < 0)
        {
            logMessage(FATAL, "服务器端ip与port与套接字绑定失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(3);
        }
        logMessage(NORMAL, "绑定成功!......");
        // UDP到这一步初始化完毕,但是TCP还存在一步,需要进行连接
        // 因为TCP是面向连接的,我们正式通信前需要先建立连接
        // 此时就相当于设置_sock套接字为监听模式了
        if (listen(_listen_sock, gbacklog) < 0)
        {
            logMessage(FATAL, "连接失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(4);
        }
    }

    // 初始化完就是启动了
    void start()
    {
        // 服务器先接收消息,然后在发送消息
        // TCP协议能否简单的像UDP那样直接进行通信吗?显然不能,在连接阶段使用的套接字是监听套接字,对信息处理并且发送是处理套接字所要干的事情
        struct sockaddr_in clientAddr;
        socklen_t clientAddrLen = sizeof clientAddr; // 用来接收客户端信息的套接字结构体
        while (true)
        {
            // 首先确保常驻
            // 首先获取连接,连接我返回,不连接在这里进行阻塞
            _service_sock = accept(_listen_sock, (struct sockaddr *)&clientAddr, &clientAddrLen);
            if (_service_sock == -1)
            {
                logMessage(ERROR, "连接客户端失败,重新连接... %d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                continue;
                // 连接失败不是致命的错误,重连一次即可
            }
            logMessage(NORMAL, "客户端连接成功.....");
            // 现在使用堆客户端信息处理的代码即可,这里我将他们封装为一个函数
            // 首先在外面先获取客户端的ip和端口
            std::string clientIp = inet_ntoa(clientAddr.sin_addr); // 将网络字节序的网络地址ip转化为点分十进制的字符串
            uint16_t clientPort = ntohs(clientAddr.sin_port);      // 网络转为本地字节序,注意是16位整数

            // 处理信息
            handleTheClient(_service_sock, clientIp, clientPort);
        }
    }

    ~TCPServer()
    {
        if (_listen_sock != -1)
            close(_listen_sock);
        if (_service_sock != -1)
            close(_service_sock);
    }

private:
    std::string _server_ip;
    uint16_t _server_port;
    int _listen_sock;  // 监听套接字
    int _service_sock; // 处理套接字
};

#endif

TCPServer.cpp

        Then call create in this file.

#include "TCPServer.hpp"
#include <memory>

static void UserManual()
{
    std::cout << "please:./TCPServer ip port or /TCPServer port" << std::endl;
}

int main(int argc, char* argv[])
{
    std::string ip = "";
    uint16_t port;
    if (argc == 2)
    {
        port = atoi(argv[1]);
    }
    else if (argc == 3)
    {
        ip = argv[1];
        port = atoi(argv[2]);
    }
    else
    {
        UserManual();
        exit(-1);
    }

    std::unique_ptr<TCPServer> TCP_server(new TCPServer(port, ip));
    TCP_server->initTCPServer();
    TCP_server->start();
    return 0;
}

TCPClient.cpp 

        For the client, first of all we do not encapsulate it.

        We have already introduced in UDP that due to the particularity of the client, let the operating system bind the ip and port for us by default, so the client only needs to create a socket and initialize the socket structure on the server side. Can. The above steps are still the same, but from now on UDP can be received directly - datagram, no connection.

        TCP is based on connections and byte streams, so naturally the client first needs to establish a connection with the server. (Three-way handshake) After the connection is established, the client's socket can communicate with the server normally. At this point, the client's socket can be regarded as an ordinary file descriptor, and it is readable and writable, so file operations can be used together.

        Of course, the client needs to disconnect from the server at the end. (waves four times)

        Let's briefly introduce the interface function for the client to connect to the server: connect

connect

header file :

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

Function prototype :

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

Function introduction :

        The connect() system call connects the socket referenced by the file descriptor sockfd to the address specified by addr.

        sockfd: client socket

        addr: server-side socket structure

        addrlen: server-side socket structure size

        Return value: 0 is returned if the connection is successful, otherwise -1 is returned.

#include "log.hpp"
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <stdlib.h>
#include <errno.h>

static void UserManual()
{
    std::cout << "please:./TCPclient server_ip server_port" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        UserManual();
        exit(-1);
    }

    // 创建客户端的套接字
    int clientSock = socket(AF_INET, SOCK_STREAM, 0);
    if (clientSock < 0)
    {
        logMessage(FATAL, "套接字创建失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(1);
    }

    // TCP客户端同样的不需要主动绑定自己的ip和port
    // 对于TCP客户端来说,需要的是连接能力,那么一个套接字足以
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof server_addr);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));  // 是要发送到网络上的,所以千万别忘了转为网络字节序
    if (0 > inet_aton(argv[1], &server_addr.sin_addr))  // 主机转网络 ip 点分十进制
    {
        logMessage(FATAL, "ip从本地转化网络字节序失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(2);
    }
    socklen_t sever_len = sizeof server_addr;
    if ( 0 > connect(clientSock, (struct sockaddr*)&server_addr, sever_len))
    {
        logMessage(FATAL, "服务器连接失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(3);
    }
    logMessage(NORMAL, "服务器连接成功~");

    // 客户端不断向服务器端发送信息接收信息即可
    std::string message;
    char buffer[1024];
    while (true)
    {
        printf("请输入# ");
        std::getline(std::cin, message);
        if (message == "quit") break;

        // 使用send可以发送
        if ( 0 > send(clientSock, message.c_str(), message.size(), 0))  // 阻塞发送
        {
            logMessage(ERROR, "客户端发送失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            continue;
        }

        // 使用read接收
        ssize_t n = read(clientSock, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << "server# " << buffer << std::endl;
        }
        else if (n == 0)
        {
            // 此时对端关闭,我也关闭即可
            break;
        }
        else
        {
            logMessage(FATAL, "接收服务器数据失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(4);
        }
    }
    close(clientSock);
    return 0;
}

        The above is the general process and ideas of TCP to realize socket programming. The basic interface has been introduced, and the effect will not be demonstrated here. If there are follow-up supplements, I will continue to write at this time. If there are any mistakes, please point them out!

        In addition, for TCP, we can use netstat -antp to view the service of the tcp protocol in the Linux environment, l is to only view the monitoring status, t is tcp, and u is udp.

        There is a tool that can simply replace the TCP client: telnet If not, please use sydo yum install telnet to install it. Enter to run, ctrl+] + quit to exit.

Guess you like

Origin blog.csdn.net/weixin_61508423/article/details/129086875