TCP/IP网络编程基础

admin 2023-10-10 715 阅读 0评论

网络协议: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

喜欢就支持以下吧
点赞 0

发表评论

快捷回复: 表情:
aoman baiyan bishi bizui cahan ciya dabing daku deyi doge fadai fanu fendou ganga guzhang haixiu hanxiao zuohengheng zhuakuang zhouma zhemo zhayanjian zaijian yun youhengheng yiwen yinxian xu xieyanxiao xiaoku xiaojiujie xia wunai wozuimei weixiao weiqu tuosai tu touxiao tiaopi shui se saorao qiudale qinqin qiaoda piezui penxue nanguo liulei liuhan lenghan leiben kun kuaikule ku koubi kelian keai jingya jingxi jingkong jie huaixiao haqian aini OK qiang quantou shengli woshou gouyin baoquan aixin bangbangtang xiaoyanger xigua hexie pijiu lanqiu juhua hecai haobang caidao baojin chi dan kulou shuai shouqiang yangtuo youling
提交
评论列表 (有 0 条评论, 715人围观)

最近发表

热门文章

最新留言

热门推荐

标签列表