【网络】网络编程相关基础

网络知识基础

网络是如何构建的

我们知道,网络可以跨地域进行主机间通信,使多个主机之间完成数据共享。对于一小块局域网,想实现网络通信并不困难,但对于长距离网络通信,解决网络传输问题就成了关键

  • 网络通信长距离传输,数据包丢失怎么办?
  • 如何定位要发送的主机?
  • 怎么进行数据转发,如何选择路径(路由)?
  • 每个网络硬件生产厂商生产出来的硬件各不相同怎么办?

这些不同性质,不同模块的问题,需要我们在设计时采用分层的结构,根据不同性质的问题,策划不同的解决方案,让解决方案 “高内聚,低耦合!” ,于是就有了网络层状结构!

对于不同的层面,网络双方需要进行 “协商”约定好数据如何转发,而操作系统有很多种,计算机的生产厂商有很多种,网络硬件设备也有很多种,如何让这些不同生产厂商生成的计算机进行通信,就需要制定一个协议标准,这就是网络协议

网络协议层状结构

OSI七层模型

在这里插入图片描述

TCP/IP协议

TCP/IP通信协议采用了五层层级结构(不过物理层一般不考虑,所以也可以算作四层),自顶向下完成网络需求
在这里插入图片描述

  • 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层
  • 数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太
    网、令牌环网, 无线LAN等标准. 交换机(Switch)工作在数据链路层
  • 网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层
  • 传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机
  • 应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层.

传输层,网路层,数据链路层共同组成了网络协议栈!(一般不考虑物理层)

网络传输流程

  • 数据的发送:自顶(应用层)向下发送
  • 数据的接收:自底(硬件层)向上接收

在这里插入图片描述

数据包的封装和分用

  • 不同的协议层都要将自己的数据包进行封装,而封装实际上就是将数据(有效载荷) 以及 该层协议(报头) 进行组合
  • 将组合好的数据包转发给下一层,下一层继续封装
  • 接收端每一个网络协议层读取数据包时,将报头和有效载荷分离,将有效载荷交付给上层,上层继续执行该过程,直到应用层成功接收数据,这就是数据包的分用

在这里插入图片描述

网络的地址管理

IP地址

IP协议有两个版本,IPv4和IPv6,默认情况下我们都是指IPv4

  • IP地址是在IP协议中, 用来标识网络中不同主机的地址
  • 对于IPv4来说,IP地址是一个4字节,32位的整数
  • 通常也使用 “点分十进制” 的字符串表示IP地址,例如 192.168.0.1用点分割的每一个数字表示一个字节,范围是 0 - 255;

MAC地址

  • MAC地址用来识别数据链路层中相连的节点
  • 长度为48位,即6个字节, 一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19)
  • 在网卡出厂时就确定了,不能修改,mac地址通常是唯一的(虚拟机中的mac地址不是真实的mac地址,可能会冲突,也有些网卡支持用户配置mac地址)

网络编程

网络端口号

端口号(port)是传输层的协议内容

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪个进程来处理
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
  • 一个端口号只能被一个进程占用

一个进程可以绑定多个端口号,但是一个端口号只能被一个进程绑定

源目IP地址和源目端口号

我们知道,网络通信其实本质就是进程通信,而在网络中,我们用IP地址 + 端口号标识某一台主机中的某一个进程,所以在网络通信中,发送端的进程需要有自己的源IP地址和源端口号,接收端进程需要有自己的目的IP地址和目的端口号

UDP协议和TCP协议

在网络传输层有两个常用协议,UDP协议和TCP协议,他们两个分别有自己的特点,这里先简单说明,之后也会详细介绍

  • UDP
  • 无连接
  • 不可靠传输
  • 面向数据报
  • TCP
  • 有连接
  • 可靠传输
  • 面向字节流

网络字节序

我们知道,在内存中,数据存储分为大端字节序和小端字节序,在网络中数据流也同样分大小端,在将本地主机的数据转发到网络中时,我们不清楚大小端是否匹配,因此我们要将主机字节序转换为网络字节序

通过以下函数进行转换

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);
  • h表示host(主机),n表示network(网络),l表示32位长整数(long),s表示16位短整数(short)
  • 如果我们要进行网络发送,就用通过htonl或htons函数将主机字节序转换为网络字节序
  • 然后通过ntohl或ntohs进行网络数据接收

套接字(Socket)

在网络通信中,我们用IP地址 + 端口号标识某一台主机中的某一个进程,而IP地址和端口号就共同组成了套接字(Socket)

socket接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
			socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
			socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
			socklen_t addrlen);

sockaddr结构

sockaddr和sockaddr_in用来存储网络通信中的地址

sockaddr

struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

sockaddr_in

#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family
  
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };
  
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };
  • sockaddr_in中sin_family字段(第一个宏字段)表示地址类型,IPv4为AF_INET,IPv6为AF_INET6
  • sin_port和sin_addr都必须是网络字节序,因此必须将主机字节序进行转换

IP地址和端口号的转换方法

#include <arpa/inet.h>
// port转换方法(前面已经提过)
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

// ip转换方法
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// inet_aton是将ip地址字符串(点分十进制)
// 和sockaddr_in.sin_addr结构体作为参数传入
int inet_aton(const char *cp, struct in_addr *inp);// 常用
// inet_addr直接将ip地址字符串(点分十进制)传入,并返回ip地址的网络字节序
in_addr_t inet_addr(const char *cp);// 常用
int inet_pton(int family, const char* strptr, void *addrptr)
// 以上是字符串转in_addr的函数

char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
// 以上是in_addr转字符串的函数

在这里插入图片描述

  • sockaddr_in解决了sockaddr的缺陷,它将IP地址和端口号进行了分离
  • sockaddr和sockaddr_in长度相同,而socket的相关接口中,参数类型都是sockaddr类型,因此我们通常使用sockaddr_in作为函数参数时,只需要进行强制类型转换就可以了(C实现多态)

简单实现UDP服务器

// 网络四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>
#include <iostream>
#include <strings.h>

class UdpServer
{
public:
    UdpServer()
    {
    }

    void InitServer() // 初始化服务器
    {
        // 1.创建socket
        // type(第二个参数) -- SOCK_DGRAM表示UDP
        sock_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock_ < 0)
        {
            exit(-1); // socket创建失败直接退出
        }

        // 给服务器指明IP和port
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 使用sockaddr_in之前最好先清空
        local.sin_family = AF_INET;
        // 服务器不需要指明一个确定的IP,因此将IP地址字段设置为INADDR_ANY
        // 如果是服务端
        // local.sin_addr_s.addr = inet_addr(serverip)
        // or inet_aton(serverip, &server.sin_addr) .的优先级比&高
        
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(port_); // 要将主机字节序转为网络字节序!

        // 2.绑定(bind)到内核中
        int n = bind(sock_, (struct sockaddr *)&local, sizeof(local)); // 将sockaddr_in强转成sockaddr
        if (n < 0)
        {
            exit(-1); // bind失败直接退出
        }
    }

    void Start()
    {
        // 服务器运行 -> 接收数据
        while (true) // 服务器是一直运行的
        {
            char buffer[1024];
            // 3.接收数据
            // 接收数据,就要知道对方的socket,因此需要创建一个临时的sockaddr_in来存储客户端信息
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 这里一定要写未来传入缓冲区的大小
            // sizeof(buffer) - 1是因为在C/C++中,字符串是以\0结尾,但在内核中不是,所以需要减1
            int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);

            // 4.业务处理
            // TODO
            // 这里以打印消息为例子

            // 提取客户端信息并打印发送的消息
            // 网络字节序转主机字节序
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;

            // 5.发数据
            // sendto();
        }
    }

    ~UdpServer()
    {
    }

private:
    int sock_; // 套接字
    uint16_t port_;
};

简单实现TCP服务器

TCP和UDP使用有点不同,对于服务端来说,TCP需要一个listensock来进行监听连接,并获取对方的socket;对于客户端来说,需要进行connect连接操作

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <strings.h>

class TcpServer
{
public:
    TcpServer()
    {
    }

    void InitServer() // 初始化服务器
    {
        // 1.创建socket
        // type(第二个参数) -- SOCK_STREAM表示TCP
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            exit(-1); // socket创建失败直接退出
        }

        // 给服务器指明IP和port
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 使用sockaddr_in之前最好先清空
        local.sin_family = AF_INET;
        // 服务器不需要指明一个确定的IP,因此将IP地址字段设置为INADDR_ANY
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(port_); // 要将主机字节序转为网络字节序!

        // 2.绑定(bind)到内核中
        int n = bind(listensock_, (struct sockaddr *)&local, sizeof(local)); // 将sockaddr_in强转成sockaddr
        if (n < 0)
        {
            exit(-1); // bind失败直接退出
        }

        // 3.监听listen(与UDP不同)
        int backlog = 32; // 这个参数再讲TCP协议时会讲到
        int n = listen(listensock_, backlog);
    }

    void Start()
    {
        // 服务器运行
        while (true) // 服务器是一直运行的
        {

            char buffer[1024];
            // 4.获取连接accept(与UDP不同)
            // 对于客户端来说,多了一个connect的过程
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 这里一定要写未来传入缓冲区的大小

            int socket = accept(listensock_, (struct sockaddr *)&peer, &len); // 同样要强转
            if (socket < 0)
            {
                // 获取失败要重新继续获取,而不是退出结束
                continue;
            }

            int n = recv(socket, buffer, sizeof(buffer) - 1, 0);

            // 5.业务处理
            // TODO
            // 这里以打印消息为例子

            // 提取客户端信息并打印发送的消息
            // 网络字节序转主机字节序
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;

            // 6.发数据
            // send();
        }
    }

    ~TcpServer()
    {
    }

private:
    // 与UDP不同,TCP需要一个listen套接字用来监听连接
    int listensock_; // 监听套接字
    uint16_t port_;
};
  • 这里的UDP和TCP服务器目前只能处理一个连接,后续可以使用多进程、多线程或线程池进行完善修补
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值