走进 C/C++后台开发的第五步: Linux网络编程

在这里插入图片描述


一. TCP/IP协议

协议即: 通信双方必须遵循的规则, 由ISO规定, RFC 文档。

1. OSI 七层模型 与 TCP/IP 四层模型

在这里插入图片描述


2. TCP/IP 协议族的成员们

  • 应用层(http 超文本传输协议 ftp 文件传输协议, telnet 远程登录协议, ssh 安全外壳协议, smtp 简单邮件发送协议, pop3 接收邮件协议)
  • 传输层 (tcp 传输控制协议, udp 用户数据包协议
  • 网络层 (ip 网络互联协议, icmp 网络控制消息协议, igmp 网络组管理协议)
  • 网络接口层(arp 地址转换协议, rarp 反向地址转换协议, mpls 多协议标签交换)

每一层负责的工作:

  • 网络接口层: 负责将二进制流转换为数据帧,并进行数据帧的发送和接收, 数据帧是独立的网络信息传输单元。
  • 网络层:负责将数据帧封装层IP数据报,并运行必要的路由算法。负责点到点的传输(主机或路由器)。
  • 传输层: 负责端对端之间的通信会话连接和建立,传输协议的选择根据数据传输的方式而定,负责端到端的传输(源主机与目的主机)
  • 应用层: 负责应用程序的网络访问,通过端口号来识别各个不同的进程。

3. ip协议与路由器

其中网络层的 ip 协议是构成 Internet 的基础, 互联网的主机通过IP地址来表示,且互联网上有大量路由器
负责根据IP地址选择合适的路径转发数据包。

路由器是在工作在网络层的网络设备,同时兼有交换机的功能,可以在不同的链路层接口之间转发数据包。

IP 协议不保证数据传输的可靠性,数据包在传输过程中可能丢失, 可靠性可用在上层协议或应用程序中提供支持。


4.每一层相关协议的注解

“IP地址和MAC地址相同点是它们都唯一,不同的特点主要有: 对于网络上的某一设备,如一台计算机或一台路由器,其IP地址是基于网络拓扑设计出的,同一台设备或计算机上,改动IP地址是很容易的(但必须唯一),而MAC则是生产厂商烧录好的,一般不能改动。

  • ARP: (地址转换协议)用于获得同一物理网络中的硬件主机地址(MAC),是设备通过知道的IP地址获得主机不知道的物理地址的协议。

  • RARP: (反向地址转换协议)允许局域网的物理机器从网关服务器的 ARP表或缓存上请求其IP地址。

  • IP :(网际互联协议)负责在主机和网络之间寻址和路由转发数据包。

  • ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。

  • IGMP:(网络组管理协议)被 IP 主机用来向本地多路广播路由器报告主机组成员的的协议,主机与本地路由器之间使用 Internet
    组管理协议来进行组播组成员信息的交互。

  • TCP:(传输控制协议)为用户提供可靠的通信连接。 适合于一次传输大批数据的情况,并适用于要求得到响应的应用程序。

  • UDP: (用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证,适用于一次传输少量数据。

在这里插入图片描述



二. 网络相关概念


1. socket 概念

Linux 中的网络编程是通过 socket 接口来进行的。

特点:

  • socket 是一种特殊的I/O接口,也是一种文件描述符
  • socket 是常用的进程间通信机制(同机器或非同机器之间均可)

组成:

  • 每一个socket 都用一个半相关描述{ 协议, 本地地址,本地端口} 表示,完整的套接字使用一个 相关描述{协议,本地地址,本地端口, 目的地址,目的端口} 表示
  • socket 类似于文件的函数调用,使用其返回一个socket描述符,随后的连接建立,数据传输等操作均通过 socket来实现。

2. socket 类型

(1) 流式 socket(SOCKET_STREAM) -> 用于 TCP 通信
(2) 数据报 socket(SOCKET_DGRAM) -> 用于 UDP通信
(3) 原始 socket(SOCKET_RAW)-> 用于新的网络协议实现的测试等

原始套接字允许对底层协议如IP和ICMP进行直接访问,功能强大但使用不便,主要用于一些协议的开发。


3. socket 信息数据结构

#include <netinet/in.h>
struct sockaddr {
    
    
  unsigned short sa_family; //地址族
  char sa_data[14];         //14字节的协议地址包含其socket的IP地址于端口号
};

struct sockaddr_in {
    
     //在sockaddr上进行了划分,使得端口,ip分开(常用)
   short int sin_family;
   unsigned short int sin_port;
   struct in_addr sin_addr; 
   unsigned char sin_zero[8];  //填充的字节,与sockaddr保持一样大小
};

struct in_addr {
    
    
   in_addr_t s_addr; //32位ipv4地址,网络字节序
}

sin_family: AF_INET-> IPV4 协议      AF_INET6 -> IPV6协议

4.数据存储优先顺序的转换

在计算机数据存储有两种字节优先顺序:高位字节优先(大端模式)和低位字节优先

  • 内存的低地址存储数据的低字节,高地址存放数据的高字节的方式叫小端模式。
  • 内存的高地址存储数据的低字节,低地址存放数据的高字节的方式叫大端模式

例如: 10 在 内存中存放的是 0x00000000a;

大端模式即: 0x00000000a
小端模式即: 0x0a0000000

可以用 char* 取一个int类型的变量的地址(用10),得到如果解引用后是10,那么是小端,是0则是大端
因为指针总是取一个变量的低地址开始(栈的地址是向下增长)

在这里插入图片描述

但是呢,在网络中的端口号和IP地址都是以网络字节序(大端法),因此我们需要对这两个变量的字节存储优先顺序进行相互转化。
这里用到4个函数:

#include <netinet/in.h>
uint16_t htons(unit16_t host16bit);  从host16位比特转换位 network 的short类型(用于端口转换)
uint16_t ntohs(unit16_t net16bit);    反转
unit32_t htonl(unit32_t host32bit);  从host32位比特转换为 network的long 类型(用于ip转换)
unit32_t ntohl(unit32_t net32bit);    反转

成功:返回转换的字节序
失败:返回-1

5. 地址格式转换

我们在表达地址时经常采用的是点分十进制表示的数值,而socket 编程中使用的是32位的网络字节序的二进制值
,这就需要对其两个数值进行转换。

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


in_addr_t inet_addr(const char *straddr); //将点分十进制转换为 32位二进制的网络字节序地址
char *inet_ntoa(struct in_addr inaddr); //将32位二进制网络字节序地址转换为点分十进制


三. Socket 编程


1. 使用TCP协议的流程图

在这里插入图片描述



服务器端:


1. 头文件

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

2. socket 函数: 生成套接口描述符

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

参数:

  • domain { AF_INET: IPV4 协议, AF_INET4: IPV6 网络协议}
  • type { SOCK_STREAM: TCP SOCK_DGRAM: UDP }
  • protocol : 指定socket 所使用的传输协议编号, 通常为0

返回值: 成功返回套接口描述符,失败返回 -1;

演示:

int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1) {
    
    
  perror("socket");
  exit(-1); 
}

3. bind 函数:用来绑定一个端口号和 IP地址,使套接口与指定的端口号和IP地址相关联,

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

参数:

  • sockfd : 为前面socket的返回值
  • my_addr: 为结构体指针变量 (可使用 sockaddr_in 输入其信息,并强制转换为 sockaddr)
  • addrlen : sockaddr 的结构体长度,通常是计算 sizeof(struct sockaddr);

返回: 成功返回0,失败返回 -1

演示:

struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(struct sockaddr_in));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8888);
my_addr.sin_addr.s_addr = inet_addr("192.168.2.100")

if(bind(sfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1)
{
    
    
   perror("bind");
   close(sfd);
   exit(-1);;
}

快捷方式:

  • 通过将 my_addr.sin_port 置为 0,函数会自动选择未占用的端口使用,
  • 通过将 my_addr.sin_addr.s_addr 置为 INADDR_ANY,系统会自动填入本机IP地址

4. listen 函数:使服务器的这个端口和 IP处于监听状态,等待网络中客户端的连接请求,如果
客户端有连接请求,端口就会接收这个连接。

int listen(int sockfd, int backlog);

参数:

  • sockfd : 为socket的返回值,即 sfd
  • backlg: 指定同时能处理的最大连接要求,通常为 10 或 5,最大值可设置 12(半连接队列的大小,第一次握手后将客户端syn加入)

返回值: 成功返回0,失败则返回 -1

常用实例:

if(listen(sfd, 10) == -1) {
    
    
  perror("listen");
  close(sfd);
  exit(-1);
}

5. accept函数: 接收远程客户端的连接请求,并将该请求放于等待队列,闲暇时刻返回该 socket 标识符,用其可与
连接的客户端 socket 进行通信。

(当accept 返回时,返回的socket代表已经完成3次握手的客户端)

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

参数:

  • sockfd 为前面 socket 的返回值,即 sfd
  • addr:为结构体变量,和 bind 的结构体是同类型,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
  • addrlen: 表示结构体的长度,为整型指针

返回值: 成功则返回新的 socket 处理代码 new_fd, 失败返回 -1

实例:

struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr *)&clientaddr, &addrlen);
if(new_fd == -1) {
    
    
  perror("accept");
  close(new_fd);
  exit(-1);
}


6. recv 函数:用与客户端通信的socket来接收远端主机传来的数据,并把数据存到由参数buf指向的内存空间。

int recv(int sockfd, void *buff, int len, unsigned int flags);

参数:

  • sockfd: 为前面accept的返回值,即 new_fd, 也就是新的套接字
  • buf: 表示缓冲区
  • len: 表示缓冲区的长度
  • flags: 通常为 0

返回值: 成功返回实际接收到的字符数,可能少于你所指定的接收长度。 失败返回 -1

实例:

char  buf[512] = {
    
    0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1) {
    
    
  perror("recv");
  close(new_fd);
  close(sfd);
  exit(-1);
}

7. send函数: 用新的套接字发送数据给指定的远端主机

int send(int client_fd, const void *msg, int len, unsigned int flags);

参数:

  • client_fd: 为accept 的返回值,
  • msg:一般为常量字符串
  • len:表示长度
  • flags:通常为 0

返回值: 成功则返回实际传送出去的字符数, 可能少于你指定的发送长度,失败则返回 -1

常用实例:

if(send(new_fd, "hello", 6,0) == -1) {
    
    
  perror("send");
  close(new_fd);
  close(sfd);
  exit(-1);
}

8. close函数:当使用完文件后若已不再需要则可使用 close()关闭该文件,并且 close() 会让数据写回磁盘

int close(int fd);

参数:

  • fd : 为前面的 sfd

返回值: 若文件顺利关闭返回 0, 发生错误时返回 -1

注意:close 的 fd, 遵循引用计数原则,dup机制


客户端:

  1. connect 函数: 用来连接远程服务器。
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

参数:

  • sockfd : 为前面 socket 的返回值,即 sfd
  • serv_addr: 为结构体指针变量,存放远程服务器的 ip 和端口信息
  • addrlen: 表示结构体变量的长度

返回值: 成功则返回0,失败返回 -1



2. 设置套接口选项 setsockopt 的用法

函数原型:

#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数:

  • sockfd : 标识一个套接口的描述符
  • level: 选项定义的层次,支持 SOL_SOCKET, IPPROTO_TCP, IPPROTO_IP. IPPROTO_IPV6
  • optname:需要设置的选项
  • optval:指针,指向存放选项值的缓冲区
  • optlen:optval 缓冲区长度

在这里插入图片描述
在这里插入图片描述

注意: 该设置全部必须要放在 bind 之前

1. 服务器关闭后象继续重用 socket,(正常是需要经历 TIME_WAIT 的过程)

int reuse = 1;
setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(int))

2. 在 send(), recv() 过程中有时由于网络状况等原因,发收不能预期进行,需要设置收发时限

int nNetTimeout = 1000; //1s
//发送时限
setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&nNetTimeout, sizeof(int))
//接收时限
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&nNetTimeout, sizeof(int));

3. 设置缓冲区大小,避免 send(), 和 recv() 不断的循环收发

int  nRecvBuf = 32*1024;
setsockopt(socket, SOL_SOCKET, SO_RCVBUF, (const char *)&nRecvBuf, sizeof(int));

int nSendBuf = 32*1024;
setsockopt(socket, SOL_SOCKET, SO_SNDBUF, (const char *)&nSendBuf, sizeof(int));

4. 一般在发送UDP数据报的时候,希望 socket 发送的数据具有广播特性。

int  bBroadcast = 1;
setsockopt(socket, SOL_SOCKET, SO_BROADCAST, (const char *)&bBroadcast, sizeof(int));

3. socketpair 创建在该进程中线程间通信的 socket

在这里插入图片描述



4. 五种I/O编程模型


1. 同步与异步,阻塞与非阻塞

同步与异步在进行I/O操作是针对应用程序与内核的交互而言的。

  • 同步过程中进程出发I/O操作会等待(阻塞)或轮询地去查看 I/O操作(内核中进行处理I/O)(非阻塞)是否完成。
  • 异步过程中进程触发 I/O 操作以后,直接返回,做自己的事情, I/O操作由内核处理,完成后内核通知进程I/O进程完成

阻塞与非阻塞关注的是单个进程的执行状态。

  • 阻塞:进程给cpu传达任务后,就一直等待cpu处理完成,然后才执行后面的操作
  • 非阻塞: 进程给cpu传达任务后,继续进行后续操作,隔断时间询问之前的操作是否完成。

2. IO模型

在这里插入图片描述


1. 阻塞 I/O模型

在这里插入图片描述

2. 非阻塞I/O模型

在这里插入图片描述

3. IO 复用模型

在这里插入图片描述

4. 信号驱动 I/O 模型

在这里插入图片描述

5. 异步 IO 模型

在这里插入图片描述



5. Epoll 多路复用

epoll 是 select 和 poll 的增强版本,是实现 I/O多路复用的主要方式


1. 主要接口:

#include <sys/epoll.h>
int epoll_create(int size);  
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

2. 接口详解



1. int epoll_create(int size);

创建一个 epoll 的实例,返回实例的文件描述符, size参数已经没有意义,但需要大于0.

注意: epoll 创建后会占用一个fd, 使用完 epoll 后,必须调用 close() 关闭。



2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event (*)event)

epoll 的事件注册函数,第一个参数是 epoll 描述符, 第二个表示 动作,有三个宏

EPOLL_CTL_ADD: 注册新的fd 到 epfd 中
EPOLL_CTL_MOD: 修改已经注册的 fd 的监听事件
EPOLL_CTL_DEL: 从epfd 中删除一个fd。

第三个参数是需要监听的 fd, 第四个参数是告诉内核要监听什么事件,其结构是:

struct epoll_event {
    
    
  __unit32_t events;
  epoll_data_t data;
}

events 可以是下面几个宏的集合:

EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断
EPOLLET: 将EPOLL设置为边缘触发(Edge Triggered)模式,相对于水平触发来说的
EPOLLONESHOT: 只监听一次事件,监听完后,就删除该描述符

水平触发与边缘触发

区别:

  • LT 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处
    理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
  • ET 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理
    该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。

选择ET

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在
ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文
件描述符的任务饿死。



3. int epoll_wait(int epfd, struct epoll_event (*)events, int maxevents, int timeout);

等待事件的产生, 参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size,参数 timeout 是超时时
间(毫秒,0 会立即返回,-1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,
如返回 0 表示已超时。

猜你喜欢

转载自blog.csdn.net/chongzi_daima/article/details/108308923