Linu网络编程

Linux Web Server(网络服务器)

什么是 Web Server(网络服务器)

一个 Web Server就是一个服务器软件(程序), 或者是运行这个服务器软件的硬件(计算机)。其主要功能是通过 HTTP 协议与客户端(通常是浏览器)进行通信,来接收,存储,处理来自客户端的 HTTP 请求,并对其请求做出 HTTP 相应,返回给客户端其请求的内容(文件、网页)或返回一个 Error 信息。

怎么样与 Web Server 通信

通常用户使用 Web 浏览器与相应的服务器进行通信。在浏览器中键入****域名或者IP 地址:端口号,浏览器则先将你的域名解析成相应的 IP 地址或者直接根据你的 IP 地址向对应的 Web 服务器发送一个 HTTP 请求。这一过程首先要通过 TCP 协议的三次握手建立与目标 Web 服务器的连接,然后 HTTP 协议生成针对目标 Web 服务器的 HTTP 请求报文,通过 TCP、IP 等协议发送到目标 Web 服务器上。

TCP/IP协议族

RFC(Request For Comments)

应用层负责处理应用程序的逻辑,数据链路层、网络层和传输层负责处理网络通信细节

TCP/IP协议族体系结构和主要协议

  • 四层协议系统
  • 自底向上分别是数据链路层、网络层、传输层和应用层
  • 上层协议使用下层协议提供的服务

数据链路层

  • 实现了网卡接口和网络驱动程序,为了处理数据在物理媒介上的传输
  • ARP(Address Resolve Protocol, 地址解析协议),将 IP 地址转换为 MAC 地址
  • RARP(Reverse Address Resolve Protocol,逆地址解析协议),将 MAC 地址转换为 IP 地址
  • ARP 和 RARP 实现了 IP 地址和物理地址(MAC 地址)的相互转换
  • 数据链里层使用 MAC 地址寻找一台机器

网络层

  • 实现数据包的选路和转发
  • WAN(广域网),LAN(局域网)
  • 通信的两台主机一般不是直接相连,通过中间多个节点相连
  • 网络层任务选取这些中间节点,以确定两台主机之间的通信路径
  • IP 协议根据数据包的目的 IP 地址来决定如何投递它,如果数据包不能直接发送给目标主机,IP 协议就会为它寻找一个合适的下一跳路由器,并将数据包交给该路由器负责数据包的转发
  • IP 协议使用逐跳方式确定通信路径
  • ICMP 协议(Internet Control Message Protocol 因特网控制报文协议),用于检测网络连接 8 位类型字段用于区分报文类型
    • 差错报文,主要回应网络错误,比如目标不可达, 重定向
    • 查询报文,用来查询网络信息,比如 ping 就是查看目标是否可达

传输层

  • 为两台主机上的应用程序提供端到端的通信,传输层只关心通信店的起始端和目的端,不在乎数据包的转发过程。
  • TCP 协议(传输控制协议),为应用层提供可靠的面向连接的和基于流的服务
    • 超时重传
    • 数据确认
    • 确保数据包被正确的发送到目的地
    • 可靠,使用 TCP 通信的双方需要先建立 TCP 连接
    • 基于流的数据没有长度限制
  • UDP 协议(用户数据报协议),为应用层提供不可靠、无连接和基于数据报的服务
    • 不可靠:UDP 无法保证数据从发送端正确地传送到目的端。****如果数据中途丢失,或者目的端通过数据校验发现数据错误而将其丢弃,UDP 只会简单的通知应用程序发送失败
    • 使用 UDP 协议需要自己做数据处理确认,超时重传等逻辑
    • UDP 是无连接的,通信双方不会保持一个长久的联系
    • 基于数据报的服务,每个 UDP 数据报有一个长度,接收端必须以该长度为最小单位将其所有内容一次性读出,否则数据将会被截断
  • SCTP 协议(流控制传输协议)

应用层

  • ping 应用程序,利用 ICMP 报文检测网络连接
  • talnet 协议是一种远程登录协议
  • OSPF,开放最短路径优先协议是一种动态路由更新协议,用于路由器之间的通信
  • DNS,域名服务协议提供机器域名到 IP 地址的转换

通过cat /etc/services查看所有应用层协议

封装

每层协议都将在上层数据的基础上加上自己的头部信息(有时还包括尾部信息),以实现该层的功能,此过程叫封装

经过 TCP 封装后的数据叫做 TCP 报文段

image-20220719173910402

**当发送端使用 send 或者 write 函数想一个 TCP 连接写入数据时,内核中的 TCP 模块首先把这些数据复制到与该连接对应的 TCP 内核发送缓冲区中,然后 TCP 模块调用 IP 模块提供的服务,传递参数包括 TCP 头部信息和 TCP 发送缓冲区中的数据,即 TCP 报文段。 **

经过 UDP 封装后的数据为 UDP 数据报,UDP 无须为应用层数据保存副本,UDP 数据报被发送成功后,UDP 内核缓冲区中的该数据包就被丢弃了。

IP 封装后的数据叫做 IP 数据报。IP 数据报也包括头部信息和数据部分,其中数据部分就是一个 TCP 报文段、UDP 数据报和 ICMP 报文

**数据链路层封装的数据称为帧, **

image-20220719174751565

分用

当帧到达主机时,将沿着协议栈自底向上传递,各层协议依次处理帧中本层负责的头部数据,以获取所需信息,并最终将处理后的帧交给目标应用程序。

DNS工作原理

DNS 是一套分布式域名服务系统。每个 DNS 服务器上都存放着大量的机器名和 IP 地址的映射,并且是动态更新的。

image-20220719193632057

Linux使用 /etc/resolv.conf查看 DNS 服务器的 IP 地址

socket 和 TCP/IP协议族的关系

数据链路层、网络层、传输层协议在内核实现,因此 OS需要实现一组系统调用,使得应用程序能够访问这些协议提供的服务。应用程序编程接口:socket

  • 将应用程序数据从用户缓冲区中复制到TCP/UDP内核发送缓冲区,以交付内核来发送数据
  • 应用程序可以通过它们来修改内核中各层协议的某些头部信息或其他数据结构,从而控制底层通信行为

IP 协议族详解

IP服务特点

  • 无状态:IP 通信双方不同步传输数据的状态信息,可能会造成传输结果乱序和重复
  • 无连接:IP 通信双方不长久地维持对方的任何信息。上层协议每次发送数据的时候,都必须明确指定对方的 IP 地址
  • 不可靠:IP 协议不能保证 IP 数据报准确地到达接收端

IPv4 结构

image-20220719203056703

IPv4 最后一个选项字段是可变长的可选信息,包括记录路由,时间戳,松散源路由选择

IP分片

IP 头部有数据报标识、标志和片偏移

IP路由

IP模块工作流程

TCP协议详解

  • TCP 头部信息出现在每个 TCP 报文段中,用于指定通信的源端口号,目的端端口号,管理 TCP 连接,控制两个方向的数据流
  • TCP 状态转移过程。TCP 连接的任意一端都是一个状态机。在 TCP 连接从建立到断开的整个过程中,
  • TCP 数据流
  • TCP 数据流的控制

TCP 相对于 UDP 协议的特点是面向连接的、字节流和可靠传输

TCP 通信双方必须先建立连接才能进行数据传输通信

TCP 连接是全双工的,双方的数据读写可以通过一个链接进行

TCP 连接是一对一的,基于广播多播的应用程序不能使用 TCP.

发生多次写操作,TCP 会将这些数据发送到缓冲区,直到真正发送数据时,将发送的数据封装成一个或多个 TCP 报文段发送

UDP 发送端每执行一次写操作,UDP 就会将其封装成UDP 数据报并发送

image-20220719211821533

TCP 头部结构

标志位

  • URG 标志, 紧急指针
  • ACK 标志,表示确认号是否有效,携带 ACK 标志的 TCP 报文段为确认报文段
  • PSH 标志,接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间
  • RST 标志:表示要求对方重新建立连接
  • SYN 标志:请求建立一个连接
  • FIN 标志:表示通知对方本端要关闭连接了

TCP的半关闭状态:如果通信一方已完成了数据发送,但允许继续接收对方发来的数据,直到对方也发送结束报文段以关闭连接

超时重传

拥塞控制

HTTP 请求

image-20220719215346384

服务器如何接收客户端发送来的 HTTP 请求报文

Web 服务器通过socket监听来自客户端的请求

 #include <sys/socket.h>
 #include <netinet/in.h>
 /* 创建监听socket文件描述符 */
 int listenfd = socket(PF_INET, SOCK_STREAM, 0);
 /* 创建监听socket的TCP/IP的IPV4 socket地址 */
 struct sockaddr_in address;
 bzero(&address, sizeof(address));
 address.sin_family = AF_INET;
 address.sin_addr.s_addr = htonl(INADDR_ANY);  /* INADDR_ANY:将套接字绑定到所有可用的接口 */
 address.sin_port = htons(port);
 
 int flag = 1;
 /* SO_REUSEADDR 允许端口被重复使用 */
 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
 /* 绑定socket和它的地址 */
 ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));  
 /* 创建监听队列以存放待处理的客户连接,在这些客户连接被accept()之前 */
 ret = listen(listenfd, 5);

客户端会尝试去connect()服务器上正在listen的这和 port,而监听到的这些连接会排队等待被 accept()。由于用户连接请求是随机到达的异步事件,每当监听socket(listenfd)监听到新的客户连接并且放入监听队列,我们都需要告诉服务器有连接来了,accept这个连接,并分配一个逻辑单元来处理这个用户请求。而且,我们在处理这个请求的同时,还需要继续监听其他客户的请求并且分配其另一个逻辑单元来处理(并发,同时处理多个事件,线程池实现并发)。这里,服务器通过epoll这种 I/O复用技术(select, poll)来实现对监听 socket(listenfd)和连接 socket(客户请求)的同时监听。注意I/O复用虽然可以同时监听多个文件描述符,但是它本身是阻塞的,并且当有多个文件描述符同时就绪的时候,如果不采取额外措施,程序则只能按顺序处理器中就绪的每一个文件描述符,所以为了提高效率,这里通过线程池来实现并发(多线程并发),为每个就绪的文件描述符分配一个逻辑单元(线程)来处理。

服务器程序通常需要处理三类事件:I/O事件,信号及定时事件。有两种事件处理模式:

  • Reactor模式:要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生(可读、可写),若有,则立即通知工作线程(逻辑单元),将socket可读可写事件放入请求队列,交给工作线程处理。
  • Proactor模式:将所有的I/O操作都交给主线程和内核来处理(进行读、写),工作线程仅负责处理逻辑,如主线程读完成后users[sockfd].read(),选择一个工作线程来处理客户请求pool->append(users + sockfd)

通常使用同步I/O模型(如epoll_wait)实现Reactor,使用异步I/O(如aio_readaio_write)实现Proactor。但在此项目中,我们使用的是同步I/O模拟的Proactor事件处理模式。那么什么是同步I/O,什么是异步I/O呢?

从一个简单的 socket 开始

网络编程就是编写程序使得两台联网的计算机相互交换数据

什么是 socket ?

socket 是计算机之间进行通信的一中约定或者一种方式。通过 socket 约定,一台计算机可以接受其他计算机的数据,也可以向其他计算机发送数据

Unix/Linux中的 socket 是什么 ?

为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:

  • 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
  • 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。

网络连接也是一个文件,它也有文件描述符

可以通过socket()函数来创建一个网络连接或者打开一个网络文件,socket()的返回值就是文件描述符。有了文件描述符,我们可以使用普通的文件操作函数来传输数据

  • 用 read()读取从远程计算机传来的数据
  • 用write()向远程计算机写入数据
 #include <sys/socket.h>
 int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • 第一个参数:IP 地址类型,AF_INET表示 IPv4, IPv6 表示为 AF_INET6
  • 第二个参数:数据传输方式,SOCK_STREAM表示流格式,面向连接,多用于 TCP。SOCK_DGRAM表示数据报格式、无连接,多用于 UDP。
  • 第三个参数:协议,0 表示根据前面的两个参数自动推导协议类型。设置为IPPROTO_TCP和IPPTOTO_UDP,分别表示TCP和UDP。

对于客户端,服务器存在的唯一标识是一个IP地址和端口,这时候我们需要将这个套接字绑定到一个IP地址和端口上。首先创建一个sockaddr_in结构体。

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下

 struct sockaddr
 {
   sa_family_t sa_family; // 地址族类型变量
   char sa_data[14]; 
 }

image-20220721173846674

TCP/IP协议族有sockaddr_in和 sockaddr_in6两个专用socket地址结构体

 struct sockaddr_in
 {
   __SOCKADDR_COMMON (sin_);
   in_port_t sin_port;/* Port number.  */
   struct in_addr sin_addr;/* Internet address.  */ IPv4 地址结构体
 
   /* Pad to size of `struct sockaddr'.  */
   unsigned char sin_zero[sizeof (struct sockaddr)
                          - __SOCKADDR_COMMON_SIZE
                          - sizeof (in_port_t)
                          - sizeof (struct in_addr)];
 };
 
 struct in_addr
 {
   in_addr_t s_addr; // in_addr_t <=> uint32_t, Ipv4 地址,要用网络字节序表示
 };
 #include <arpa/inet.h>
 struct sockaddr_in serv_addr;
 bzero(&serv_addr, sizeof(serv_addr));

bzero包含于里, bzero() 能够将内存块(字符串)的前n个字节清零,在"string.h"头文件中**,原型为:void bzero(void s, int n);*

主机字节序和网络字节序的转换

 #include <netinet/in.h>
 htonl: host to network long 长整形的主机字节序列数据转换为网络字节序数据
 htons: host to network short
 ntohl: network to host long
 ntohs: network tp host short

设置地址族、IP地址和端口:

 serv_addr.sin_family = AF_INET;
 serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 serv_addr.sin_port = htons(8888);
 // 然后将 socket 地址与文件描述符绑定
 bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr))

为什么定义的时候使用专用socket地址(sockaddr_in)而绑定的时候要转化为通用socket地址(sockaddr),以及转化IP地址和端口号为网络字节序的inet_addrhtons等函数及其必要性,因为所有 socket 编程接口使用的地址参数的类型都是sockaddr

最后我们需要使用listen函数监听这个socket端口,这个函数的第二个参数是listen函数的最大监听队列长度,系统建议的最大值SOMAXCONN被定义为128。

 listen(sockfd, SOMAXCONN)

要接受一个客户端连接,需要使用accept函数。对于每一个客户端,我们在接受连接时也需要保存客户端的socket地址信息,于是有以下代码:

 struct sockaddr_in clnt_addr;
 socklen_t clnt_addr_len = sizeof(clnt_addr);
 bzero(&clnt_addr, sizeof(clnt_addr));
 int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len); 
 printf("new client fd %d! IP: %s Port: %d\n", clnt_sockfd, inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));
 
 inet_addr函数将用点分十进制字符串表示的 IPv4 地址转化为用网络字节序整数表示的 Ipv4地址。失败为INADDR_NONE
 inet_aton:完成和inet_addr同样的功能,但是将转化结构存储于参数inp 指向的地址结构中。成功返回 1,失败返回 0.
 inet_ntoa: 将用网络字节序整数表示的 Ipv4 地址转化为用点分十进制字符串表示 Ipv4 地址。

要注意和acceptbind的第三个参数有一点区别,对于bind只需要传入serv_addr的大小即可,而accept需要写入客户端socket长度,所以需要定义一个类型为socklen_t的变量,并传入这个变量的地址。另外,accept函数会阻塞当前程序,直到有一个客户端socket被接受后程序才会往下运行。

我们可以定一个检验错误的函数

 void errif(bool condition, const char *errmsg) {
     if (condition) {
         perror(errmsg);
         exit(EXIT_FAILURE);
     }
 }

关闭 socket 连接

 #include <unistd.h>
 int close(int fd); // close系统调用并非总是立即关闭一个连接,而是将 fd 的引用计数减1.只有当 fd 的引用计数为 0 时,才是真正关闭连接
 #include <sys/socket.h>
 int shutdown(int sockfd, int howto) // 立即关闭连接
  howto 参数
  - SHUT_RD: 关闭 sockfd 上读的这一半,应用程序不能再针对 socket 文件描述符执行读操作,并且该 socket 接收缓冲区中的数据都被丢弃
  - SHUT_WR: 关闭写的一半
  - SHUT_RDWR: 同时关闭读写

TCP 数据读写

 #include <sys/socket.h>
 #include <sys/types.h>
 
 ssize_t recv(int sockfd, void *buf, size_t len, int flags)
 ssize_t send(int sockfd, const void *buf, sise_t len, int flags)
 rev读取 sockfd 上的数据, buf:指定缓冲区的位置 len: 缓冲区的大小, flags通常设置 0
 send 往 sockfd 上写数据,send 成功时返回实际写入的数据的长度
posted @ 2022-07-22 10:50  Lilyan&Code  阅读(125)  评论(0编辑  收藏  举报