alin的学习之路(Linux网络编程:一)(网络模型、帧格式、socket套接字、服务器端实现)

alin的学习之路(Linux网络编程:一)(网络模型、帧格式、socket套接字、服务器端实现)

1. 协议

协议是一组规则,规定了如何发送数据。通信的双发都需要遵守该规则

在这里插入图片描述

2. 网络分层结构模型

在这里插入图片描述

1. OSI七层模型

物:物理层

数:数据链路层

网:网络层

传:传输层

会:会话层

表:表示曾

应:应用层

2. TCP/IP模型

链:数据链路层(网络接口层):以太网帧协议,ARP协议

网:网络层:IP协议,ICMP协议,IGMP协议

传:传输层:TCP协议,UDP协议

应:应用层:HTTP,ftp,nfs,telnet,ssh

3. 网络通信过程

数据在没有被封装之前,是不能传输到网络上去进行通信的。

数据产生后,经由数据链路层、网络层、传输层、应用层,四层每一层都会对数据进行不同的封装,封装后才能传送到网络中进行通信。

到达目的端时,经由应用层、传输层、网络层、数据链路层,四层每一层都会解封装,从而最终得到通信数据。

4. 网络协议格式

1. 以太网帧格式

  • 以太网规定,数据包必须从一块网卡,传送到另一块网卡。

  • 目的地址、源地址 —— mac地址(ifconfig命令可查看)。网卡编号。全球唯一。

  • ARP协议:
    根据 IP地址, 获取 mac 地址。

    ARP 包括请求和应答,请求携带发送端的 mac 地址,通过应答的信息来获取目的端的 mac 地址

在这里插入图片描述

2. IP段格式

  • 版本:IPv4、IPv6
  • TTL: time to live:防止路由包拥塞,表示最大路由跳数
    • 用来设置数据包,在路由节点中的跳转上限。每经过一个路由节点,该值 -1。
    • 减为0的路由节点, 有义务将该数据包丢弃。 防止拥塞网络。
  • 源IP地址:32位 —— 4字节。 192.168.1.4 —— 点分十进制 IP地址。本质 是一个 string。 为人类看。
  • 目的IP地址:32位 —— 4字节

在这里插入图片描述

3. UDP协议格式

  • 源端口号:16位。2^16 — 取值范围:0 ~ 65535
  • 目的端口号:16位。2^16 — 取值范围:0 ~ 65535
  • IP地址:在 网络环境中, 唯一标识一台主机。
  • port:在 网络主机上,唯一标识一个进程。
  • IP+port :在 网络环境中, 唯一标识一个 进程。 —— socket 套接字。

在这里插入图片描述

4. TCP协议格式

  • 源端口号:16位。2^16 — 取值范围:0 ~ 65535
  • 目的端口号:16位。2^16 — 取值范围:0 ~ 65535
  • 32位序号。
  • 32位确认序号。
  • 6个标志位。
  • 16位窗口大小。---- 取值范围:0 ~ 65535

在这里插入图片描述

TCP 优先建立好连接,后续消息全部在该路径上发送,UDP则是每次传输的路由路径不同

5. 网络应用程序设计模式

C/S 模式(client/server):

  • 优点:提前缓存数据,提高通信效率。协议选择灵活。开发调试较方便。
  • 缺点:对用户安全性构成威胁。开发工作量大。不能跨平台。
  • 应用场景:对数据安全、稳定要求较高场合。需要提前缓冲数据。

B/S 模式(browser/server):

  • 优点:不需要额外向用户主机安装应用。开发工作量相对小。跨平台使用。
  • 缺点:不能缓存大量数据。协议选择不灵活。调试不方便。
  • 应用场景:需要跨平台。

6. 网络套接字

在一次通信中,套接字socket必须成对出现

一个文件描述符指向一个套接字 ( 该套接字内部由内核借助两个缓冲区实现。)

在这里插入图片描述

1. 网络字节序

小端法:(pc本地存储,IA架构)高位存高地址,低位存低地址。
大端法:(网络存储)高位存低地址,低位存高地址。

需要在做网络传输时,将 “网络字节序” —— “本机字节序” 相互转换。

#include <arpa/inet.h>

h表示host,n表示network,l表示32位长整数,s表示16位短整数。
    
// 本地字节序(IP) ---> 网络字节序(IP)
uint32_t htonl(uint32_t hostlong);
参数:要求是一个 int, 但 “192.168.6.108” 是 string。——> atoi() --> int --> htonl() --> 网络字节序。
    
// 本地字节序(port) ---> 网络字节序(port)
uint16_t htons(uint16_t hostshort);

// 网络字节序(IP) --->本地字节序(IP)
uint32_t ntohl(uint32_t netlong);

// 网络字节序(port) --->本地字节序(port)
uint16_t ntohs(uint16_t netshort);

2. IP地址转换函数

#include <arpa/inet.h>
// 将 本机字节序 string 类型的 IP地址, 转换为 网络字节序 数值类型
int inet_pton(int af, const char *src, void *dst);
	af:AF_INET(IPv4)、AF_INET6(IPv6)
	src:传入。IP地址(string类型,点分十进制)
	dst:传出。转换后的数值类型的 IP地址。
	返回值:
		1:成功。
		0:语法正确, IP无效。
		-1:失败。
// 将 网络字节序 数值类型, 转换回程 本机字节序 string。
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
	af:AF_INET(IPv4)、AF_INET6(IPv6)
	src:传入。网络字节序 IP地址。
	dst:传出。string 类型 IP
	size:dst 的大小。
	返回值:
		成功:dst 传出值。string 类型的 IP地址。
		失败:NULL

3. sockaddr 数据结构

使用 man 7 ip 查看 struct sockaddr_in 结构体类型

struct sockaddr_in {
  sa_family_t   sin_family; /* address family: AF_INET */
  in_port_t    sin_port;  /* port in network byte order */
  struct in_addr sin_addr;  /* internet address */
};

/* Internet address. */
struct in_addr {
  uint32_t    s_addr;   /* address in network byte order */
};

// 举例:
struct sockaddr_in addr;  // 定义地址结构(IP+port)变量
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
// #define INADDR_ANY 0 --- 取系统中任意一个 有效的 IP地址。
addr.sin_addr.s_addr = htonl(INADDR_ANY);

// 模拟,使用 bind 绑定地址结构。
bind(lfd, (struct sockaddr *)&addr, sizeof(addr));

7. socket 通信流程图

在这里插入图片描述

在这里插入图片描述

8. socket 通信函数

1. socket 函数

#include <sys/types.h>  该头文件包含在 <unistd.h>
#include <sys/socket.h>

// 创建一个新的套接字。
int socket(int domain, int type, int protocol);
	domain: AF_INET、AF_INET6、AF_UNIX
	type:SOCK_STREAM、SOCK_DGRAM
	protocol:通常传 0
  	返回值:
 		成功:返回新套接字对应的 fd
 		失败:-1,errno

2. bind 函数

#include <sys/types.h>  该头文件包含在 <unistd.h>
#include <sys/socket.h>

// 给 socket 绑定IP+port
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	sockfd:socket函数的返回值。———— 监听套接字。
	addr: 地址结构体。 ———— 类型应该是:  struct sockaddr_in ,函数调用时需要强制类型转换
  	addrlen: sizeof(addr) 地址结构大小
  	返回值:
 		成功:0
 		失败:-1, errno

3. listen 函数

#include <sys/types.h>    
#include <sys/socket.h>

// 设置同时与服务器建立连接的 上限数。———— 该函数不会阻塞程序。
int listen(int sockfd, int backlog);
	sockfd: socket 函数的返回值
	backlog: 设置的监听最大数。 128
  	返回值:
 		成功:0
 		失败:-1, errno 

4. accpet 函数

// 阻塞等待客户端连接请求。 成功的话,返回一个与客户端成功建立连接的,用于通信的 socket文件描述符
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	sockfd: socket 函数的返回值
	addr: 传出参数。 成功与服务器建立连接的那个客户端地址结构。
	addrlen:传入传出参数。
	入:addr的大小。出:客户端 addr 的实际大小。
		举例:socklen_t clt_addr_len = sizeof(addr);  &clt_addr_len
	返回值:
 		成功:能与客户端进行数据通信的 socket 对应的 文件描述符。
 		失败:-1, errno

5. connect 函数

// 使用现有的 socket 与服务器建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	sockfd: socket 函数的返回值
	addr:传入参数。 服务器的地址结构。
	addrlen: 服务器地址结构的大小。
	返回值:
		成功:0
 		失败:-1, errno
———— 客户端自己的地址结构,没有指定,由系统自动分配 --- "隐式绑定" 

9. TCP-CS 模型通信

1. server端

通信流程:

  1. socket函数创建监听套接字
  2. bind函数绑定IP地址和端口号
  3. listen函数设置最大监听上限
  4. accept函数阻塞等待客户端接入服务器,返回值传出客户端的IP地址和端口号
  5. read函数从accept的返回值文件描述符中读数据
  6. 数据处理:小写转大写,使用toupper函数
  7. write函数写回到accept的返回值,即写回到客户端
  8. close函数关闭文件描述符

编码实现:

#include <stdio.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ctype.h>

#define SRV_PORT 9527
#define SIZE 4096

int main()
{
    int lfd = 0;

    struct sockaddr_in srv_addr,clt_addr;
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(SRV_PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);


    //1.创建套接字
    lfd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == lfd)
    {
        perror("socket");
        return 1;
    }

    //绑定IP和port
    int ret = bind(lfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));
    if(-1 == ret)
    {
        perror("bind");
        return 1;
    }

    //listen 设置最大监听数
    ret = listen(lfd,128);
    if(-1 == ret)
    {
        perror("listen");
        return 1;
    }

    printf("服务器阻塞等待客户端接入\n");

    //accept 阻塞等待客户端接入
    socklen_t clt_addr_len = sizeof(clt_addr_len);
    int cfd = accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);
    if(-1 == cfd)
    {
        perror("accept");
        return 1;
    }

    char ip[SIZE] = {0};
    printf("客户端接入:ip:%s,port:%d\n",
           inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,ip,SIZE),
           ntohs(clt_addr.sin_port));

    char buf[SIZE] = {0};  
    while((ret = read(cfd,buf,SIZE)) != 0)
    {
        if('\0' != buf[strlen(buf)-1])
            buf[strlen(buf)-1] = '\0';
        for(int i=0 ;i<ret ;++i)
        {
            buf[i] = toupper(buf[i]);
        }
        write(cfd,buf,ret);
        printf("client:%s\n",buf);
        memset(buf,0,SIZE);
    }

    close(cfd);
    close(lfd);

    return 0;
}

2. client端

通信流程:

  1. socket函数创建通信套接字文件描述符
  2. connect函数与服务器建立连接,传入的参数中addr表示服务器的IP地址和端口号
  3. 从标准输入中读字符串,将该字符串通过write写到通信套接字中
  4. read函数从服务器读处理后的数据
  5. 结束后关闭文件描述符

编码实现:

#include <stdio.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ctype.h>

#define SRV_PORT 9527
#define SIZE 128

int main()
{
    int cfd;

    cfd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == cfd)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in srv_addr;
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(SRV_PORT);
    inet_pton(AF_INET,"127.0.0.1",&srv_addr.sin_addr.s_addr);

    int ret = connect(cfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));
    if(-1 == ret)
    {
        perror("connect");
        return 1;
    }

    printf("客户端连接成功\n");
    char buf[SIZE];
    while(1)
    {
        fgets(buf,SIZE,stdin);
        ret = write(cfd,buf,SIZE);
        ret = read(cfd,buf,SIZE);
        if(0 == ret)
        {
            printf("客户端退出\n");
            break;
        }
    }
    close(cfd);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_41775886/article/details/107673215