网络协议:TCP/IP

图片img

  1. 应用层:主要负责为用户提供网络服务。应用层协议包括HTTP、FTP、SMTP等。
  2. 传输层:主要负责在网络中建立端到端的连接,提供可靠的数据传输。传输层协议包括TCP和UDP。
  3. 网络层:主要负责网络地址的分配和路由选择,例如 IP 协议。
  4. 数据链路层:主要负责传输数据帧,例如以太网、ATM 和 PPP 等协议。

参考资料:

https://zhuanlan.zhihu.com/p/620485741

进程间通信:IPC

  1. 管道:pipe
  2. 信号:signal
  3. 信号量:semaphore
  4. 消息队列:message
  5. 共享内存:share memory
  6. 套接字:socket

参考资料:

https://zhuanlan.zhihu.com/p/502627174

套接字:socket

图片img

  1. 服务端和客户端初始化Socket,得到文件描述符
  2. 服务端调用bind,绑定IP和端口
  3. 服务端调用listen,进行监听
  4. 服务端调用accept,等待客户端连接
  5. 客户端调用connect,向服务端发起连接请求。(TCP三次握手)
  6. 服务端调用accept返回用于传输的Socket的文件描述符(和第一点得到的Socket不同)
  7. 客户端使用write写入数据,服务端调用read读取数据
  8. 客户端断开连接时会调用close,服务端也会调用close(TCP四次挥手)

参考资料:

https://blog.csdn.net/OYMNCHR/article/details/124728256

最简单的服务端

Cygwin 安装教程:

https://blog.csdn.net/weixin_44778232/article/details/127579150

server.cpp

#include <iostream>

#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
using std::string;

int main()
{
    // 1. 创建 socket
    int sockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sockfd < 0)
    {
        printf("create socket error: errno=%d errmsg=%s\n", errno, strerror(errno));
        return 1;
    }
    else
    {
        printf("create socket success!\n");
    }

    // 2. 绑定 socket
    string ip = "127.0.0.1";
    int port = 8080;

    struct sockaddr_in sockaddr;
    std::memset(&sockaddr, 0sizeof(sockaddr));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_addr.s_addr = inet_addr(ip.c_str());
    sockaddr.sin_port = htons(port);
    if (::bind(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0)
    {
        printf("socket bind error: errno=%d, errmsg=%s\n", errno, strerror(errno));
        return 1;
    }
    else
    {
        printf("socket bind success: ip=%s port=%d\n", ip.c_str(), port);
    }

    // 3. 监听 socket
    if (::listen(sockfd, 1024) < 0)
    {
        printf("socket listen error: errno=%d errmsg=%s\n", errno, strerror(errno));
        return 1;
    }
    else
    {
        printf("socket listen ...\n");
    }

    while (true)
    {
        // 4. 接收客户端连接
        int connfd = ::accept(sockfd, nullptrnullptr);
        if (connfd < 0)
        {
            printf("socket accept error: errno=%d errmsg=%s\n", errno, strerror(errno));
            return 1;
        }

        char buf[1024] = {0};

        // 5. 接收客户端的数据
        size_t len = ::recv(connfd, buf, sizeof(buf), 0);
        printf("recv: conn=%d msg=%s\n", connfd, buf);

        // 6. 向客服端发送数据
        ::send(connfd, buf, len, 0);
    }

    // 7. 关闭 socket
    ::close(sockfd);
    return 0;
}

最简单的客户端

client.cpp

#include <iostream>

#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
using std::string;

int main()
{
    // 1. 创建 socket
    int sockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sockfd < 0)
    {
        printf("create socket error: errno=%d errmsg=%s\n", errno, strerror(errno));
        return 1;
    }
    else
    {
        printf("create socket success!\n");
    }

    // 2. 连接服务端
    string ip = "127.0.0.1";
    int port = 8080;

    struct sockaddr_in sockaddr;
    std::memset(&sockaddr, 0sizeof(sockaddr));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_addr.s_addr = inet_addr(ip.c_str());
    sockaddr.sin_port = htons(port);
    if (::connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0)
    {
        printf("socket connect error: errno=%d errmsg=%s\n", errno, strerror(errno));
        return 1;
    }

    // 3. 向服务端发送数据
    string data = "hello world";
    ::send(sockfd, data.c_str(), data.size(), 0);

    // 4. 接收服务端的数据
    char buf[1024] = {0};
    ::recv(sockfd, buf, sizeof(buf), 0);

    printf("recv: %s\n", buf);

    // 5. 关闭 socket
    ::close(sockfd);

    return 0;
}

Socket 封装

服务端:server2.cpp

#include <iostream>

#include <socket/socket.h>
using namespace yazi::socket;

int main()
{
    Singleton<Logger>::instance()->open("./../server.log");

    // 1. 创建 socket
    Socket server;

    // 2. 绑定 socket
    server.bind("127.0.0.1"8080);

    // 3. 监听 socket
    server.listen(1024);

    while (true)
    {
        // 4. 接收客户端连接
        int connfd = server.accept();
        if (connfd < 0)
        {
            return 1;
        }

        Socket client(connfd);

        char buf[1024] = {0};

        // 5. 接收客户端的数据
        size_t len = client.recv(buf, sizeof(buf));
        printf("recv: connfd=%d msg=%s\n", connfd, buf);

        // 6. 向客户端发送数据
        client.send(buf, len);
    }

    // 7. 关闭 socket
    server.close();
    return 0;
}

客户端:client2.cpp

#include <iostream>

#include <socket/socket.h>
using namespace yazi::socket;

int main()
{
    // 1. 创建 socket
    Socket client;

    // 2. 连接服务端
    client.connect("127.0.0.1"8080);

    // 3. 向服务端发送数据
    string data = "hello world";
    client.send(data.c_str(), data.size());

    // 4. 接收服务端的数据
    char buf[1024] = {0};
    client.recv(buf, sizeof(buf));
    printf("recv: %s\n", buf);

    // 5. 关闭 socket
    client.close();

    return 0;
}

阻塞 IO

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除阻塞状态。

非阻塞 IO

当用户线程发起read操作后,并不需要等待,而是马上得到结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发起read操作。如果内核中的数据准备好了,它就将数据拷贝到用户线程。

在非阻塞IO模型中,用户线程需要不断地轮询内核数据是否就绪,也就是说非阻塞IO不会交出CPU,而会一直占用CPU。

发送缓冲区

暂时无法在飞书文档外展示此内容

socket 没法直接将数据发送到网卡,所以只能先将数据发送到操作系统数据发送缓冲区。然后网卡从数据发送缓冲区中获取数据,再发送到接收方。

如果用户程序发送数据的速度比网卡读取的速度快,那么发送缓冲区将会很快被写满,这个时候 send 会被阻塞,也就是写入发生阻塞。

接收缓冲区

暂时无法在飞书文档外展示此内容

首先接收方机器网卡接收到发送方的数据后,先将数据保存到操作系统接收缓冲区。用户程序感知到操作系统缓冲区的数据后,主动调用接收数据的方法来获取数据。

如果数据接收缓冲区为空,这个时候 recv 会被阻塞,也就是读取发生阻塞。

注意:发送缓冲区和接收缓冲区这两个区域是每一个socket连接都有的。本质上而言,就是内核中的两块内存空间,socket创建完成后,这两块内存空间就开辟出来了。

SO_LINGER

设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。

参考资料:

https://blog.csdn.net/u012635648/article/details/80279338

SO_KEEPALIVE

不论是服务端还是客户端,一方开启 KeepAlive 功能后,就会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。

参考资料:

https://www.cnblogs.com/1119reya/p/10382276.html

SO_REUSEADDR

SO_REUSEADDR是一个很有用的选项,一般服务器的监听socket都应该打开它。它的大意是允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接

参考资料:

https://zhuanlan.zhihu.com/p/79999012

服务端 socket

class ServerSocket : public Socket
{
public:
    ServerSocket() = delete;
    ServerSocket(const string &ip, int port);
    ~ServerSocket() = default;
};

客户端 socket

class ClientSocket : public Socket
{
public:
    ClientSocket() = delete;
    ClientSocket(const string & ip, int port);
    ~ClientSocket() = default;
};

IO 多路复用

在多路复用IO模型中,会有一个专门的线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。IO多路复用的优势在于,可以处理大量并发的IO,而不用消耗太多CPU/内存。

三种常用的轮询方法:select、poll、epoll