编程语言
首页 > 编程语言> > Linu网络编程

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协议族体系结构和主要协议

数据链路层

网络层

传输层

应用层

通过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

IP 协议族详解

IP服务特点

IPv4 结构

image-20220719203056703

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

IP分片

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

IP路由

IP模块工作流程

TCP协议详解

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

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

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

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

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

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

image-20220719211821533

TCP 头部结构

标志位

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事件,信号及定时事件。有两种事件处理模式:

通常使用同步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)。例如:

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

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

 #include <sys/socket.h>
 int sockfd = socket(AF_INET, SOCK_STREAM, 0);

对于客户端,服务器存在的唯一标识是一个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 成功时返回实际写入的数据的长度

标签:addr,IP,Linu,编程,网络,TCP,地址,数据,socket
来源: https://www.cnblogs.com/Lilyan/p/16504957.html