目录
一.OSI七层体系结构(重点)
1.计算机网络体系结构
存在两种国际标准:
法律上的 (de jure) 国际标准 : OSI(没有得到市场的应用);
事实上的 (de facto) 国际标准 :TCP/IP(获得了最广泛的应用);
2.协议与划分层次
(1)网络协议 (network protocol):简称为协议,是为进行网络中的数据交换而建立的规则、标准或约定
(2)协议的两种形式:
文字描述:便于人来阅读和理解
程序代码:让计算机能够理解
(3)层次式协议结构
①对于非常复杂的计算机网络协议,其结构应该是层次式的。如图所示
每个层次有特定的协议来处理特定的任务,处理好后传送给上一层或者下一层。在解决主机1某一层问题的时候只考虑主机2该层次的问题,就像是双方对应的层次在通信一样;
体系结构是抽象的,而实现则是具体的,是真正在运行的计算机硬件和软件。
②各层的主要功能有(下表不对应层次顺序,是概括)
差错控制 | 使相应层次对等方的通信更加可靠。 |
流量控制 | 发送端的发送速率必须使接收端来得及接收,不要太快。 |
分段和重装 | 发送端将要发送的数据块划分为更小的单位,在接收端将其还原。 |
复用和分用 | 发送端几个高层会话复用一条低层的连接,在接收端再进行分用。 |
连接建立和释放 | 交换数据前先建立一条逻辑连接,数据传送结束后释放连接。 |
3.OSI七层体系结构(熟练)
高层:负责主机之间的数据传输
应用层 (Applications Layer): | 网络服务与最终用户的一个接口
|
表示层 (Presentation Layer): | 数据的表示、安全、压缩
|
会话层 (Session Layer): | 建立、管理、中止会话
|
低层:负责网络之间的数据传输
运输层 (Transport Layer): | 定义传输数据的协议端口号,以及流控和差错校验
|
网络层 (Network Layer): | 进行逻辑地址寻址、差错校验等功能
|
数据链路层 (Data Link Layer): | 建立逻辑连接、进行硬件地址寻址、差错校验等功能
|
物理层 (Physical Layer): | 建立、维护、断开物理连接、
|
本部分总结提炼:
应用层:用户应用程序与网络服务交互的接口;
表示层:数据整理,包括数据的格式化、加密、压缩和解压缩,以确保在不同系统间的数据交换能够正确进行;
会话层:建立、管理、中止应用间的会话;
传输层:使用端口号来标识不同的应用程序或服务,以及流控和差错校验;
网络层:进行逻辑地址寻址、差错校验等功能;
数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能;
物理层:建立、维护、断开物理连接。
二.TCP/IP与五层体系结构(重点)
1.TCP/IP 的四层协议体系结构
(1)类比七层协议
(1)TCP/IP 的四层协议
因为路由器不是主机 ,不使用涉及主机的层级
(3)TCP/IP 体系结构的另一种表示方法
现在互联网使用的 TCP/IP 体系结构已经发生了演变,即某些应用程序可以直接使用 IP 层,或甚至直接使用最下面的网络接口层。
(4)沙漏计时器形状的 TCP/IP 协议族
2.具有五层协议的体系结构
(1)三种协议类比
(2)五层协议
各层主要功能
互联网中客户-服务器工作方式
本部分结尾总结:
五层模型体系结构每一层及其功能:
应用层:通过应用进程间的交互来完成特定网络应用。为用户提供网络服务和应用程序接口,使用户能够访问各种应用和服务;
运输层:在网络中的不同主机之间的应用提供端到端的通信,处理数据的分段、流量控制、差错检测和纠正等;
网络层:处理数据包的路由选择和分组转发,使数据能够跨越不同子网到达目标主机;
数据链路层:将原始的比特流组织成数据帧,执行错误检测和纠正,实现两个相邻节点之间的可靠通信,在两个相邻节点间的链路上传送帧;
物理层:将比特流在物理媒介上进行传输,处理与电压、光信号等有关的硬件细节,实现比特(0 或 1)的传输,确定连接电缆的插头应当有多少根引脚,以及各引脚应如何连接。
三.IP地址与端口号(重点)
1.IP 地址及其表示方法(IPV4)
(1)IP地址
(2)IP地址组成结构
IP地址 = { <网络号><主机号>};
IP地址在整个互联网范围内是唯一的。 IP 地址指明了连接到某个网络上的一个主机;
通过对32位二进制数对网络号和主机号的位数分配,可以得到以下默认分类
默认分类的IP地址
A类: 网络号共用8位表示,但是首位固定为 0
,接下来连续的7位可以自由设定,意思是A类地址的网络号有=128个();主机号为24位,意思每个网络号下面最多能有=16777216台主机,但是A 类网络地址中, 网络号 0 和 127 是保留地址,不指派。0 表示“本网络”,127 保留作为本地环回测试地址。所以A类网络数最多为126个,每个网络下面最大主机数为16777214(224 - 2)
根据以上思路归纳如下:
子网掩码:把子网掩码表示成二进制的位数就是网络号的位数,把实际ip地址表示成二进制数之后,根据前面得到网络号的位数换算成十进制就是网络号
A类的默认子网掩码为:255.0.0.0;
B类的默认子网掩码为:255.255.0.0;
C类的默认子网掩码为:255.255.255.0;
还有一些特殊的IP地址
补充: 根据如果是默认分类的IP种类,任意一个IP地址我们都可以迅速的得出类别,并计算得出网络号 当一个主机通过两个网卡同时连接到两网络时,也就是该主机同时拥有两个IP地址,该主机被称为多归属主机
一个路由器至少连接到两个不同的网络,一个路由器至少拥有两个IP地址;
2.划分子网
一个拥有1000台主机的组织,需要申请哪类IP地址?
根据上面的表格,要满足需求至少需要申请一个B类地址,但是B类一个网络号下面可以容纳65534,而我们只用1000个主机,这就会导致超过64000个地址没有被使用,很浪费。此时如果把主机号的位数减少变成表示子网的位数,这样就可以在一个网络号下面分配子网,子网下面的主机数量就会减少了。随着加入互联网的组织数量的迅速增加,IP地址面临被分配完的危险 为了解决上述问题,IETF提出了划分子网的编址改进方案;
给IP地址划分为三个结构
IP 地址 = { <网络号>, <子网号>, <主机号>};
思路就是调整网络号、子网号、主机号的位数达到理想需求的状态,使得每个网络号下分配的子网的主机达到理想中的需求;
在网络号确定好下来以后,主机数就已经确定了,要想划分子网,就把主机的位数分给子网,子网最大数量就是2的分配过来的位数次方;
3.无分类编址 CIDR
CIDR (Classless Inter-Domain Routing) :无分类域间路由选择。
消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念,可以更加有效地分配 IPv4 的地址空间,但无法解决 IP 地址枯竭的问题。
IP 地址 = { <网络前缀>, <主机号>}
思路结合了默认分类IP和划分子网,这种编址方式不按照默认那种规定死的,按照实际需求出发来申请,使得一个网络号下面
上图中表示前20位表示网络前缀(相当于子网掩码) ,把前二十位的二进制数表示出来然后转换出来是这个地址的网络号,然后剩余12位是用来表示主机号的,相当于是直接算主机数;
所以根据ip地址和网络前缀的位数(子网掩码的另一种性质)才能确定一个网络号和主机号;
4.IPv6 的地址(了解)
(1)IPv6 的地址:冒号十六进制记法
(2)零压缩
5.TCP和UDP以及端口
(1)运输层的TCP和UDP协议
TCP协议:全双工可靠信道,面向连接,TCP 传送的数据单位协议是 TCP 报文段 (segment)。
提供可靠的、面向连接的运输服务。 不提供广播或多播服务。 开销较多。
UDP协议:不可靠协议,UDP 传送的数据单位协议是 UDP 报文或用户数据报。传送数据之前不需要先建立连接。 收到 UDP 报后,不需要给出任何确认。 不提供可靠交付,但是一种最有效的工作方式。
(2) 一个IP地址可以唯一地标识一个主机,但是一个主机上面有很多个应用程序,这个时候就必须使用端口号标识出来是哪个应用程序,而这个工作是运输层做的。在运输层使用协议端口号 (protocol port number),或通常简称为端口 (port)。把端口设为通信的抽象终点。所以两个计算机中的进程要互相通信,不仅必须知道对方的端口号,而且还要知道对方的 IP 地址
(3)端口用一个 16 位端口号进行标志,允许有 65,535 个不同的端口号。 端口号只具有本地意义,只是为了标志本计算机应用层中的各进程。 在互联网中,不同计算机的相同端口号没有联系。
(4) 端口分类:端口分为三种类型端口
一种端口是众所周知的知名端口:一般这些端口都是一些固定的应用服务,因为这些应用服务是常用的,所以固定下来便于其他计算机访问
例如:
80:HTTP (超文本传输协议)
443:HTTPS (超文本传输安全协议)
25:SMTP (简单邮件传输协议)
110:POP3 (邮局协议版本3)
53:DNS (域名系统)
21:FTP (文件传输协议)
第二种是
注册端口:这些端口号范围在1024到49151之间,用于用户自定义的服务或应用程序;
第三种是
私有端口:也称临时端口,端口号范围在49152到65535之间,是由操作系统动态分配的端口号范围,通常在49152到65535之间.当计算机系统上的应用程序需要进行网络通信时,操作系统会从这个范围内选择一个可用的端口,并在通信结束后释放它,以供其他应用程序使用.这样可以避免端口冲突并优化资源利用.
四.字节序及IP地址转换 (重点)
1.主机字节序和网络字节序
(1)什么是字节序
字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序,分为: 大端字节序 (Big endian)
小端字节序(Little endian)
大端字节序是从高位开始存储和计算,低位字节序是从低位开始存储和计算,大端字节序符合人的使用习惯,小端字节序符合计算机内部的计算原理;
一般主机当中使用小端字节序
网络通信当中必须使用大端字节序
(2)查看主机字节序
uint32_t val32 = 0x11223344;
uint8_t val8 = *( (uint8_t *)&val32 );
if(val8 == 0x44)
printf("本机是小端字节序\n");
else
printf("本机是大端字节序\n");
定义了一个无符号32位整型变量val32,并且初始化为0x11223344
定义一个8位的无符号整型变量val8,并且将强转以后的val32的数据赋值给val8
这样做的目的是获取变量val32第一个字节的值,后面就可通过判断val8的值是0x44或者0x11来确定主机字节序是大端还是小端
2.字节序转换函数
//头文件
#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);
本机转网络 | 网络转主机 | |
32位数据 | htonl | ntohl |
16位数据 | htons | ntohs |
3.IP地址转换函数
点分十进制 :字符串形式
32位转点分十进制
//32位数据转字符串
#include <arpa/inet.h>
//IP地址序转换函数
char* inet_ntoa(struct in_addr in);
int inet_ntop(int af, const void *addr, char *cp);
点分十进制转32位
//字符串转32位数据
#include <arpa/inet.h>
//IP地址序转换函数
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *addr);
int inet_pton(int af, const char *cp, void *addr);
IPV6的地址转换函数
//支持IPV6的地址转换函数
#include <arpa/inet.h>
//IP地址序转换函数
int inet_pton(int af, const char *cp, void *addr);
int inet_ntop(int af, const void *addr, char *cp);
总结:
字节序有大小端之分,计算机内部计算采用小端字节序,但是按照人类的思维和应用场景我们一般习惯于大端字节序,因此网络通信使用大端字节序,即网络字节序。
处理方法:
在数据发送之前,需要将数据转换为网络字节序;在数据接收后,需要将数据从网络字节序转换为主机字节序。在C语言中,可以使用以下函数进行字节序的转换:
htons():主机字节序转换为网络字节序(16位数据)。
htonl():主机字节序转换为网络字节序(32位数据)。
ntohs():网络字节序转换为主机字节序(16位数据)。
ntohl():网络字节序转换为主机字节序(32位数据)。网络字节序转主机字节序以后ip地址通常需要转换成“点分十进制”的字符串
五.socket套接字及TCP的实现框架(重点)
1.socket套接字
(1)几种常见的网络编程接口(实现网络通信的方法)
●Berkeley UNIX 操作系统定义了一种 API,它又称为套接字接口 (socket interface)
●微软公司在其操作系统中采用了套接字接口 API,形成了一个稍有不同的 API,并称之为 Windows Socket
●AT&T 为其 UNIX 系统 V 定义了一种 API,简写为 TLI (Transport Layer Interface)
(2)socket
(3)套接字类型
●流式套接字 (SOCK_STREAM) 提供可靠的、面向连接的通信流;它使用TCP,从而保证数据传输的可靠性和顺序性
●数据报套接字 (SOCK_DGRAM) 定义了一种不可靠、面向无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP
●原始套接字(SOCK_RAW) 允许直接访问底层协议,如IP或ICMP,它功能强大但使用较为不便,主要用于协议开发。
2.socket常用API
(1)套接字基本函数
/*创建套接字*/
int socket(int domain, int type, int protocol);
/*绑定通信结构体*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*监听套接字*/
int listen(int sockfd, int backlog);
/*处理客户端发起的连接,生成新的套接字*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*向服务器发起连接请求*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(2)详解基本函数
socket() 创建一个新的套接字,并返回套接字描述符。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain: 套接字的协议族,如 AF_INET 表示IPv4协议族。
type: 套接字的类型,如 SOCK_STREAM 表示TCP套接字,(数据报套接字:SOCK_DGRAM)
protocol: 使用的协议,通常为0表示自动选择。
bind() 将套接字绑定到指定的IP地址和端口。(使用实际配置结构体配置以后转成struct sockaddr *通用地址类型)(这里只介绍了ipv4的地址结构体,ipv6地址结构体也是配置完了然后转成通用,这样就让一个函数能够接收不同类型的结构体)
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 套接字描述符。
addr: 指向 struct sockaddr 结构体的指针,其中包含要绑定的IP地址和端口。
addrlen: addr 结构体的大小。
struct sockaddr //通用地址族结构体
{
sa_family_t sa_family;
char sa_data[14];
};
/***IPV4***/
struct sockaddr_in //实际配置时用到的结构体 这个结构体用于表示IPv4地址,包括IP地址和端口号
{
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // 网络字节序的端口号
struct in_addr sin_addr; // IPv4地址结构体
};
struct in_addr // IPv4地址结构体原型
{
uint32_t s_addr;
};
/***IPV6***/
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in_addr sin_addr;
uint32_t sin6_scope_id;
};
struct sockaddr
{
unsigned char s6_addr[16];
};
listen() 开始监听传入的连接请求
当调用 listen
函数后,操作系统会在套接字上创建一个连接队列(backlog),用于存放尚未被 accept
函数接受的连接请求。
当客户端尝试连接到服务器时,服务器的套接字处于监听状态,操作系统将传入的连接请求放入连接队列中。如果连接队列已满(即队列中的连接请求数达到了系统限制),新的连接请求将被拒绝或丢弃,直到有空间可用。
一旦连接请求被接受(通过调用 accept
函数),操作系统会从连接队列中取出一个连接请求,创建一个新的套接字来处理该连接,并将其移出连接队列。因此,listen
函数将传入的连接请求放入队列,但并不直接处理这些连接请求,而是由 accept
函数来接受和处理它们。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd: 套接字描述符。
backlog: 等待连接队列的最大长度,通常为一个整数。
accept() 接受传入的连接请求,创建一个新的套接字用于与客户端通信。
这个套接字将被用于与客户端进行通信。这个新的套接字是专门为了与特定客户端建立通信而创建的,它独立于原始的监听套接字。
一旦新的套接字被创建,服务器可以使用它来与客户端进行数据交换,而原始的监听套接字则继续等待新的连接请求。这种方式允许服务器同时处理多个客户端的连接请求,并在每个连接上提供服务。
- 从连接队列中取出一个连接请求。
- 创建一个新的套接字来处理该连接,并返回该套接字的文件描述符。
- 原始的套接字继续监听传入的连接请求。
- 在创建新套接字后,服务器端可以利用该套接字与客户端进行通信。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 套接字描述符,函数socket生成的套接字。
addr: 指向 struct sockaddr 结构体的指针,用于存储连接客户端的地址信息。
addrlen: addr 结构体的大小,作为输入输出参数。
connect() 用于客户端向服务器发起连接请求,并在连接成功后返回与服务器通信的套接字。
是在客户端调用的函数,用于建立与服务器端的连接。具体来说,connect
函数的主要作用包括:
- 向指定的服务器地址和端口发起连接请求。
- 如果连接成功建立,
connect
函数将返回,并客户端可以通过返回的套接字与服务器进行通信。 - 如果连接失败,
connect
函数将返回一个错误码,客户端可以根据错误码进行相应的处理。
调用 connect
函数会阻塞当前线程,直到连接成功建立或者发生错误。如果连接成功建立,客户端可以利用返回的套接字来与服务器进行数据交换,发送请求并接收响应。如果连接失败,客户端可以根据返回的错误码进行错误处理,例如重试连接、提示用户等。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 套接字描述符。
addr: 指向 struct sockaddr 结构体的指针,包含要连接的服务器地址和端口。
addrlen: addr 结构体的大小。
(3)三元组
【IP地址,端口,协议】
IP地址:标识计算机
端口号:标识计算机当中的进程
协议:指定数据传输的方式
3.TCP通信的实现过程
六.实现TCP通信(重点)
1.简单实现
server.c 服务端代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 5001
#define BACKLOG 5
int main(int argc, char *argv[])
{
int fd, newfd;
char buf[BUFSIZ] = {}; //BUFSIZ 8142
struct sockaddr_in addr;//配置ipv4的地址族结构体
/*创建套接字*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(0);
}
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);//主机转网络
addr.sin_addr.s_addr = 0;//自己的ip地址
/*绑定通信结构体*/
if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("bind");
exit(0);
}
/*设置套接字为监听模式*/
if(listen(fd, BACKLOG) == -1)
{
perror("listen");
exit(0);
}
/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
newfd = accept(fd, NULL, NULL);
if(newfd < 0)
{
perror("accept");
exit(0);
}
printf("BUFSIZ = %d\n", BUFSIZ);
read(newfd, buf, BUFSIZ);
printf("buf = %s\n", buf);
close(fd);
return 0;
}
client.c 客户端代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 5001
#define BACKLOG 5
#define STR "Hello World!"
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_in addr;
/*创建套接字*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(0);
}
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");//要访问的ip,这个ip是本机
/*向服务端发起连接请求*/
if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("connect");
exit(0);
}
write(fd, STR, sizeof(STR) );
printf("STR = %s\n", STR);
close(fd);
return 0;
}
2.进阶
server.c 服务端代码 输入./server 0 8888 即可运行
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define BACKLOG 5
int main(int argc, char *argv[])
{
int fd, newfd, ret;
char buf[BUFSIZ] = {}; //BUFSIZ 8142
struct sockaddr_in addr;
if(argc < 3)
{
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
/*创建套接字*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(0);
}
addr.sin_family = AF_INET;
addr.sin_port = htons( atoi(argv[2]) );//argv是字符格式
if ( inet_aton(argv[1], &addr.sin_addr) == 0)
{
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*绑定通信结构体*/
if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("bind");
exit(0);
}
/*设置套接字为监听模式*/
if(listen(fd, BACKLOG) == -1)
{
perror("listen");
exit(0);
}
/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
newfd = accept(fd, NULL, NULL);
if(newfd < 0){
perror("accept");
exit(0);
}
while(1)
{
memset(buf, 0, BUFSIZ);
ret = read(newfd, buf, BUFSIZ);
if(ret < 0)
{
perror("read");
exit(0);
}
else if(ret == 0)
break;
else
printf("buf = %s\n", buf);
}
close(newfd);
close(fd);
return 0;
}
client.c 客户端代码
输入./client.c 127.0.0.1 8888 即可运行
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define BACKLOG 5
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_in addr;
char buf[BUFSIZ] = {};
if(argc < 3)//检查传递参数数量
{
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
/*创建套接字*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(0);
}
addr.sin_family = AF_INET;
addr.sin_port = htons( atoi(argv[2]) );
if ( inet_aton(argv[1], &addr.sin_addr) == 0)
{
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*向服务端发起连接请求*/
if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("connect");
exit(0);
}
while(1)
{
printf("Input->");
fgets(buf, BUFSIZ, stdin);
write(fd, buf, strlen(buf) );
}
close(fd);
return 0;
}
3.Makefile(编译方便)
C=gcc
CFLAGS=-Wall
all: client server
client: client.c
$(CC) $(CFLAGS) client.c -o client
server: server.c
$(CC) $(CFLAGS) server.c -o server
clean:
rm client server
七.TCP并发实现(熟练)
1.TCP多进程
(1) server.c 服务器端代码
父进程和子进程之间各自拥有自己的文件描述符,可以独立地进行通信,不会产生干扰。父进程只负责接受连接请求和创建子进程,而子进程负责实际的通信。
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <sys/wait.h>
#define BACKLOG 5
void SigHandle(int sig)
{
if(sig == SIGCHLD)
{
printf("client exited\n");
wait(NULL);
}
}
void ClinetHandle(int newfd);
int main(int argc, char *argv[])
{
int fd, newfd;
struct sockaddr_in addr, clint_addr;
socklen_t addrlen = sizeof(clint_addr);
#if 0
struct sigaction act;
act.sa_handler = SigHandle;
act.sa_flags = SA_RESTART;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
#else
signal(SIGCHLD, SigHandle);
#endif
pid_t pid;
if(argc < 3)
{
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
/*创建套接字*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(0);
}
addr.sin_family = AF_INET;
addr.sin_port = htons( atoi(argv[2]) );
if ( inet_aton(argv[1], &addr.sin_addr) == 0)
{
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*地址快速重用*/
int flag=1,len= sizeof (int);
if ( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1)
{
perror("setsockopt");
exit(1);
}
/*绑定通信结构体*/
if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("bind");
exit(0);
}
/*设置套接字为监听模式*/
if(listen(fd, BACKLOG) == -1)
{
perror("listen");
exit(0);
}
while(1)
{
/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
newfd = accept(fd, (struct sockaddr *)&clint_addr, &addrlen);
if(newfd < 0)
{
perror("accept");
exit(0);
}
printf("addr:%s port:%d\n", inet_ntoa(clint_addr.sin_addr), ntohs(clint_addr.sin_port) );
if( (pid = fork() ) < 0)
{
perror("fork");
exit(0);
}
else if(pid == 0)
{
close(fd);
ClinetHandle(newfd);
exit(0);
}
else
close(newfd);
}
close(fd);
return 0;
}
void ClinetHandle(int newfd)
{
int ret;
char buf[BUFSIZ] = {};
while(1)
{
//memset(buf, 0, BUFSIZ);
bzero(buf, BUFSIZ);
ret = read(newfd, buf, BUFSIZ);
if(ret < 0)
{
perror("read");
exit(0);
}
else if(ret == 0)
break;
else
printf("buf = %s\n", buf);
}
close(newfd);
}
(2)client.c 客户端代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define BACKLOG 5
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_in addr;
char buf[BUFSIZ] = {};
if(argc < 3)
{
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
/*创建套接字*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(0);
}
addr.sin_family = AF_INET;
addr.sin_port = htons( atoi(argv[2]) );
if ( inet_aton(argv[1], &addr.sin_addr) == 0)
{
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*向服务端发起连接请求*/
if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("connect");
exit(0);
}
while(1)
{
printf("Input->");
fgets(buf, BUFSIZ, stdin);
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
2.TCP多线程
(1)server.c 服务器端代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <pthread.h>
#define BACKLOG 5
void *ClinetHandle(void *arg);
int main(int argc, char *argv[])
{
int fd, newfd;
struct sockaddr_in addr, clint_addr;
pthread_t tid;
socklen_t addrlen = sizeof(clint_addr);
if(argc < 3)
{
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
/*创建套接字*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(0);
}
addr.sin_family = AF_INET;
addr.sin_port = htons( atoi(argv[2]) );
if ( inet_aton(argv[1], &addr.sin_addr) == 0)
{
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*地址快速重用*/
int flag=1,len= sizeof (int);
if ( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1)
{
perror("setsockopt");
exit(1);
}
/*绑定通信结构体*/
if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("bind");
exit(0);
}
/*设置套接字为监听模式*/
if(listen(fd, BACKLOG) == -1)
{
perror("listen");
exit(0);
}
while(1)
{
/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
newfd = accept(fd, (struct sockaddr *)&clint_addr, &addrlen);
if(newfd < 0)
{
perror("accept");
exit(0);
}
pthread_create(&tid, NULL, ClinetHandle, &newfd);
pthread_detach(tid);//线程分离,结束后自行回收
}
close(fd);
return 0;
}
void *ClinetHandle(void *arg)
{
int ret;
char buf[BUFSIZ] = {};
int newfd = *(int *)arg;
while(1)
{
//memset(buf, 0, BUFSIZ);
bzero(buf, BUFSIZ);
ret = read(newfd, buf, BUFSIZ);
if(ret < 0)
{
perror("read");
exit(0);
}
else if(ret == 0)
break;
else
printf("buf = %s\n", buf);
}
printf("client exited\n");
close(newfd);
return NULL;
}
(2)client.c 客户端代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define BACKLOG 5
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_in addr;
char buf[BUFSIZ] = {};
if(argc < 3)
{
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
/*创建套接字*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(0);
}
addr.sin_family = AF_INET;
addr.sin_port = htons( atoi(argv[2]) );
if ( inet_aton(argv[1], &addr.sin_addr) == 0)
{
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*向服务端发起连接请求*/
if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("connect");
exit(0);
}
while(1)
{
printf("Input->");
fgets(buf, BUFSIZ, stdin);
write(fd, buf, strlen(buf) );
}
close(fd);
return 0;
}
八.实现UDP通信(熟练)
1.接收和发送接口函数
TCP通信一般用的read和write,UDP一般用send/recv或者sendto与recvfrom
发送:
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
接收:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t read(int fd, void *buf, size_t count);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
常见flags:
1. 一般设置为0
2. MSG_PEEK:窥视传入的数据。 数据被复制到缓冲区中,但不会从输入队列中删除。
3. MSG_OOB:处理带外(OOB)数据
4.sendto与recvfrom最后两个参数是通信结构体和结构体的宽度;
2.UDP实现过程
UDP通信不是面向连接的,服务端不需要套接字监听(listen)和接收(accept)跟客户端的连接,所以客户端的connect也不需要,因此UDP通信不保证对方能接收到;
3.代码实现
(1)server.c 服务端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_in addr;
char buf[BUFSIZ] = {};
socklen_t addrlen = sizeof(addr);//计算通信结构体的长度
if(argc < 3)//判断输入参数是否合法
{
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(EXIT_FAILURE);
}
/*创建套接字*/
if( (fd = socket(AF_INET, SOCK_DGRAM, 0) ) < 0)
{
perror("socket");
exit(EXIT_FAILURE);
}
/*设置通信结构体*/
bzero(&addr, sizeof(addr) );//清空结构体
addr.sin_port = htons( atoi(argv[2]) );//要访问的端口号
if(inet_aton(argv[1], &addr.sin_addr) == 0) //要访问的ip
{
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
while(1)
{
bzero(buf, BUFSIZ);
printf("Input->");
fgets(buf, BUFSIZ, stdin);
sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&addr, addrlen);//发送信息
}
close(fd);
return 0;
}
(2) client.c 客户端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_in addr;//定义通信结构体
char buf[BUFSIZ] = {};
if(argc < 3)//入口参数检查
{
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(EXIT_FAILURE);
}
/*创建套接字*/
if( (fd = socket(AF_INET, SOCK_DGRAM, 0) ) < 0)
{
perror("socket");
exit(EXIT_FAILURE);
}
/*设置通信结构体*/
bzero(&addr, sizeof(addr) );
addr.sin_port = htons( atoi(argv[2]) );
if(inet_aton(argv[1], &addr.sin_addr) == 0)
{
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*绑定通信结构体*/
if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1)
{
perror("bind");
exit(EXIT_FAILURE);
}
while(1)
{
bzero(buf, BUFSIZ);
recvfrom(fd, buf, BUFSIZ, 0, NULL, NULL);//接收信息
printf("buf=%s\n", buf);
}
close(fd);
return 0;
}
九.TCP协议是如何实现可靠传输的(重点)
1.TCP最主要的特点
TCP 是面向连接的运输层协议,在无连接的、不可靠的 IP 网络服务基础之上提供可靠交付的服务。为此,在 IP 的数据报服务基础之上,增加了保证可靠性的一系列措施。
(1)每一条 TCP 连接只能有两个端点 (endpoint),每一条 TCP 连接只能是点对点的(一对一)
(2)TCP 提供可靠交付的服务。
(3)TCP 提供全双工通信。
(4)面向字节流
TCP 中的“流”(stream) 指的是流入或流出进程的字节序列。
面向字节流:虽然应用程序和 TCP 的交互是一次一个数据块,但 TCP 把应用程序交下来的数据看成仅仅是一连串无结构的字节流。
TCP是基于字节流的协议,这意味着它不关心传输的数据是什么类型的数据(例如文本、图像、音频等)。它将数据视为一连串的字节,而不考虑数据的边界或结构。发送端将数据流分割成小块,然后传输给接收端,接收端负责重新组装这些字节以还原原始数据。
这种基于字节流的特性使得TCP非常灵活,适用于各种不同类型的应用,但也需要应用层协议来处理数据的边界和结构,以确保数据的正确解析和处理。
注意:
Socket 有多种不同的意思
应用编程接口 API 称为 socket API, 简称为 socket。
socket API 中使用的一个函数名也叫作 socket。
调用 socket 函数的端点称为 socket。
调用 socket 函数时其返回值称为 socket 描述符,可简称为 socket。
2.TCP是如何实现可靠传输的
(1)序号与确认:TCP使用序号来对每个发送的字节进行编号,每个字节都有一个唯一的序号。接收端收到数据后,会发送一个确认消息,确认已经成功接收到的字节,这个确认消息包含了期望接收的下一个字节的序号。如果发送端在一定时间内没有收到确认消息,它会重新发送未确认的数据。
(2)超时与重传:TCP使用超时机制来检测数据包是否丢失。如果发送端在规定的时间内没有收到确认消息,它会认为数据包已经丢失,然后重新发送这个数据包。超时时间通常根据网络延迟动态调整,以确保在不同网络条件下的可靠性。
以下图是各种出错情况下双方的应对机制
(3)流量控制:TCP通过使用滑动窗口协议来进行流量控制。发送端和接收端都维护一个窗口大小,用于控制可以发送多少数据。这个窗口大小可以根据网络状况进行动态调整,以避免数据拥塞和丢失。
(4)拥塞控制:TCP还实现了拥塞控制机制,以避免网络拥塞导致数据丢失。拥塞控制算法会监测网络的拥塞情况,并根据情况减小发送速率,以降低网络负载,从而减少丢包的可能性。
(5)有限状态机:TCP的状态机定义了连接的不同状态,如建立连接、数据传输、终止连接等。这个状态机确保了连接的正确建立和终止,以及在不同状态下的数据传输行为。
TCP实现可靠传输通过序号与确认、超时与重传、流量控制、拥塞控制以及有限状态机等多种机制来确保数据的完整性和可靠性。这些机制共同工作,使得TCP能够在不可靠的网络环境中提供高度可靠的数据传输服务,确保数据不会丢失、乱序或损坏。
以上TCP可靠传输机制的实现依赖于TCP报文段的组成格式
3.TCP报文段的数据格式
5x4=20字节
源端口(Source Port):16位字段,表示发送方自己的端口号。这个字段用于标识发送方的应用程序。
目标端口(Destination Port):16位字段,表示接收方的端口号。这个字段用于标识接收方的应用程序。
序列号(sequence Number):32位字段,用于对数据字节进行编号,以便接收方按正确的顺序重新组装数据流。在建立连接时,这个字段的值会被初始化,然后每次发送数据时都会递增。
确认号(acknowledgment Number):32位字段,用于确认已经成功接收到的数据字节。它包含了发送方期望接收的下一个字节的序列号。
数据偏移(Data Offset):4位字段,指示TCP首部的长度,以32位字为单位。这个字段用于确定TCP首部的结束位置,以便正确解析首部后面的数据。
保留位(Reserved):6位字段,保留供将来使用,目前必须设置为0
URG(紧急标志):1位,表示紧急指针字段是否有效。
ACK(确认标志):1位,表示确认号字段是否有效。
PSH(推送标志):1位,表示接收方应该立即将接收到的数据交给应用层而不进行缓冲。
RSTRE(复位标志):1位,用于复位连接。
SYN(同步标志):1位,用于建立连接。
FIN(结束标志):1位,用于释放连接。
窗口大小(Window Size):16位字段,表示接收方的接收窗口大小,用于流量控制。发送方根据这个字段来控制发送速率,以避免数据拥塞。
校验和(Checksum):16位字段,用于检测TCP首部和数据的错误。接收方使用校验和来验证接收到的数据是否正确。
紧急指针(Urgent Pointer):16位字段,仅当URG标志位被设置时才有效。它指示了紧急数据的结束位置。
选项(Options):TCP首部可以包含一些可选的选项,这些选项用于提供额外的控制信息,如最大报文段长度(MSS)、时间戳等。
填充(Padding):TCP首部中的选项字段长度是可变的,为了使首部长度是32位的整数倍,可能需要添加填充字段。
十.TCP连接管理与UDP协议(重点)
1.连接的建立——“三次握手”
TCP 建立连接的过程叫做握手。
采用三报文握手:在客户和服务器之间交换三个 TCP 报文段,以防止已失效的连接请求报文段突然又传送到了,因而产生 TCP 连接建立错误。意思是如果客户端向服务器发送一个建立连接的请求,但该请求在网络中滞留并在一段时间后到达服务器,如果没有三次握手,服务器可能会错误地建立连接。通过三次握手,服务器可以验证客户端的连接请求是最近的,而不是一个过期的请求。
三次握手的意义在于确保双方能分别知晓对方正常发送和接收没问题,建立可靠的连接,从而确保了后续数据的正常传输。第一次握手,在服务端知晓客户端的发送没问题,第二次握手在客户端知晓服务器的接收和发送没问题,第三次握手在服务端知晓客户端的接收没问题(因为第二次握手是由服务端发过去的,有对应的第三次说明,第二次客户端是正常接收到了的)
如果一次握手:在一次握手中,客户端向服务器发送连接请求,并建立连接。然而,由于缺乏服务器的确认,客户端无法确定服务器是否正确地接收到了请求,不知是否已经连接上,无法保证连接正常。
如果两次握手:在两次握手中,客户端发送连接请求,服务器收到请求后向客户端发送确认,之后即可建立连接。虽然这样确保了连接成功,但如果第二次服务器给客户端的确认在网络中延时或者丢失了,客户端就以为没有连接上,会再次发起一个连接增加服务端的资源消耗。如果增加了第三次握手,服务器收到了第三次的握手,服务器就知道,客户端已经知道它自己已经连上服务器了,不用再发起连接了。如果第三次没有收到,服务器就会再向客户端发送连接成功的信号,使客户端不要再发起连接了。
2.连接的释放——“四次挥手”
四次挥手的主要目的是确保双方都能够安全、可靠地关闭TCP连接,同时还需要确保在关闭连接后不会有任何未处理的数据或重复的数据在网络中滞留。
第一次挥手客户端向服务端说明了不再发送消息,请求关闭连接,第二次挥手,表明收到了客户端的结束请求,之后如果这个时候服务端还有数据就会发送数据,发送完了服务端就会第三次握手说他不再发数据,请求关闭连接。第四次握手客户端确认收到了服务器的结束请求,此时向服务器表明连接关闭完成。
想看到三次握手和四次挥手的具体内容可以通过wireshark来抓包
3.UDP协议
UDP是一种面向无连接的传输层协议,它与TCP一样位于OSI模型中的传输层。与TCP不同,UDP不提供像连接管理、流量控制和拥塞控制等可靠性和顺序性特性。UDP通常用于那些对传输速度要求较高、可容忍一些数据丢失的应用场景,而TCP等协议更适合要求可靠性的通信。
UDP是一种面向报文的传输层协议,这意味着UDP将应用程序交给它的数据视为一系列独立的报文(或数据包),而不是像TCP那样将数据视为字节流。这个概念有以下几个重要方面:
(1)独立的报文处理:UDP将每个数据包或报文视为一个独立的单元。这意味着UDP不会将多个数据包合并成一个连续的数据流,也不会将一个大的数据流拆分成多个小的数据包。每个报文都会作为一个整体传输。这种特性使得UDP适用于一些特定的应用场景,例如实时数据传输,但也意味着应用程序需要自己处理数据包的边界和重组,并承担更多的可靠性和顺序性控制责任。
(2)不提供拆分或重组:UDP不关心数据包的边界或内部结构。它不会对报文进行拆分或重组,也不会对数据包的序列进行重新排序,只会按照样发送。这意味着应用程序需要自己处理数据包的边界和重组,以确保正确解析和处理数据。接收⽅ UDP 对 IP 层交上来的 UDP ⽤户数据报,去除⾸部后就原封不动地交付上层的应⽤进程,⼀次交付⼀个完整的报⽂。
(3)低开销:由于UDP不需要维护数据流的状态信息,不需要序列号或确认号,因此UDP的头部开销相对较小。这使得UDP适用于那些对传输速度要求较高的应用,如实时音频和视频传输。
(4)无连接:UDP是一种面向无连接的协议,不需要在通信的两端建立连接。这意味着在数据传输之前不需要进行三次握手或四次挥手等连接建立和关闭过程。
UDP数据报的⾸部格式
十一.IP协议与ethernet协议
1.IP协议(重点)
(1)IP地址的意义和作用
IP协议的主要作用是实现数据包在网络中的路由、寻址和传递,以确保数据能够从发送方传输到接收方。当互联网上的主机之间进行通信时,就好像在一个网络上通信一样,看不见互连的各具体的网络异构细节。 如果在这种覆盖全球的 IP 网的上层使用 TCP 协议,那么就是现在的互联网 (Internet)。
(2)IP 数据报首部格式
版本:占 4 位,指 IP 协议的版本。 目前的 IP 协议版本号为 4 (即 IPv4)。
首部长度:占 4 位,可表示的最大数值 是 15 个单位(一个单位为 4 字节), 因此 IP 的首部长度的最大值是 60 字节。
区分服务:占 8 位,用来获得更好的服务。 只有在使用区分服务(DiffServ)时,这个字段才起作用。 在一般的情况下都不使用这个字段。
总长度:占 16 位,指首部和数据之和的长度, 单位为字节,因此数据报的最大长度为 65535 字节。 总长度必须不超过最大传送单元 MTU。
标识 (identification) :占 16 位, 它是一个计数器,用来产生 IP 数据报的标识。
标志(flag) :占 3 位,目前只有前两位有意义。 标志字段的最低位是 MF (More Fragment)。 MF=1 表示后面还有分片,MF=0 表示最后一个分片。 标志字段中间的一位是 DF (Don't Fragment) 。 只有当 DF=0 时才允许分片。
片偏移:占 13 位,指出:较长的分组在分片后 某片在原分组中的相对位置。 片偏移以 8 个字节为偏移单位。
生存时间:占 8 位,记为 TTL (Time To Live), 指示数据报在网络中可通过的路由器数的最大值。
协议:占 8 位,指出此数据报携带的数据使用何种协议, 以便目的主机的 IP 层将数据部分上交给那个处理过程;
首部校验和(16位):首部校验和字段用于校验IP标头的完整性,以检测标头在传输过程中是否被损坏。
选项(可选,长度可变):选项字段包含了一些额外的信息,如记录路由、时间戳等。这个字段的长度可变,可以用于特殊的通信需求。
(3)IP 数据报分片
IP数据报在传输过程中可能需要分片,把一个数据报的数据部分拆分成几个数据段,然后每个加上新的首部,就变成了数据量更小的新数据报。主要是因为网络中的链路或设备具有不同的最大传输单元(MTU,Maximum Transmission Unit),而IP数据报的大小可能会超过某些链路或设备的MTU。
2.以太网协议(拓展)
以太网是数据链路层的主要协议之一,主要工作于在同一物理网络或局域网中的多台计算机和设备之间传输数据数据(具体mac设备之间传输)。链路层通常涵盖了多种协议和技术,但以太网在这一层次上是最常见和广泛使用的协议之一,尤其在有线局域网中。
以太网定义了数据帧的格式、MAC地址的使用、帧的传输和许多与数据链路层相关的细节。在大多数局域网中,无论是家庭网络、企业内部网络还是校园网络,以太网是主要的通信协议,它确保了连接到同一物理网络的多台设备能够有效地交换数据。当然,数据链路层还涵盖其他协议和技术,特别是在广域网(WAN)和无线局域网(WLAN)等不同环境中,可能会使用其他数据链路层协议。然而,在局域网环境中,以太网通常是主导的数据链路层协议。
(1)数据链路层信道类型
(2)以太网 V2 的 MAC 帧格式
目标 MAC 地址(Destination MAC Address):占据帧的前6个字节,表示数据帧的目标设备的MAC地址。这个字段标识了数据帧应该被传输到哪个设备。
源 MAC 地址(Source MAC Address):占据帧的接下来的6个字节,表示数据帧的发送设备的MAC地址。这个字段标识了数据帧的发送者。
类型/长度字段(Type/Length Field):占据帧的接下来的2个字节。在以太网 V2 中,这个字段通常用于表示数据帧中包含的协议类型,例如IPv4或IPv6。如果值小于或等于0x05DC(1518),则表示长度,而如果值大于0x05DC,则表示协议类型。
数据字段(Data Field):这个字段包含了要传输的数据,它的长度可以变化。根据类型/长度字段的值,数据字段可能包含IP数据包、ARP数据包或其他上层协议的数据。
校验和字段(Frame Check Sequence,FCS):占据帧的最后4个字节。这个字段用于存储CRC(循环冗余检验)校验和,以检测在数据帧传输过程中是否发生了错误。(算法)
以太网 V2 帧格式通常用于传输IP数据包和其他上层协议的数据。这个帧格式相对简单,并且广泛用于局域网通信。需要注意的是,以太网帧格式还可以包括一些可选字段,如VLAN标签(用于虚拟局域网)等,以满足特定网络需求。
十二.UNIX域套接字(熟练)
UNIX域套接字(UNIX Domain Socket)是一种在同一台计算机上进程(文件)之间进行通信的机制。它与网络套接字(例如TCP/IP套接字)不同,UNIX域套接字是基于本地文件系统路径的,而不是基于网络地址的。这使得它们非常适合在同一台计算机上的不同进程之间进行高效的本地通信。
1.UNIX 域流式套接字
UNIX 域流式套接字服务器端流程如下:
(1)创建 UNIX 域流式套接字。
(2)绑定本地地址(套接字文件)。
(3)设置监听模式。
(4)接收客户端的连接请求。
(5)发送/接收数据。
在bind函数介绍里面有例程
服务端代码如下:
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define MY_SOCK_PATH "/tmp/my_sock_file"
#define LISTEN_BACKLOG 50
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char *argv[])
{
int sfd, cfd;
struct sockaddr_un my_addr, peer_addr;
socklen_t peer_addr_size;
char buf[BUFSIZ] = {};
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
handle_error("socket");
memset(&my_addr, 0, sizeof(struct sockaddr_un));
my_addr.sun_family = AF_UNIX;
strncpy(my_addr.sun_path, MY_SOCK_PATH,
sizeof(my_addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *) &my_addr,
sizeof(struct sockaddr_un)) == -1)
handle_error("bind");
if (listen(sfd, LISTEN_BACKLOG) == -1)
handle_error("listen");
peer_addr_size = sizeof(struct sockaddr_un);
cfd = accept(sfd, (struct sockaddr *) &peer_addr,
&peer_addr_size);
if (cfd == -1)
handle_error("accept");
recv(cfd, buf, BUFSIZ, 0);
printf("%s\n", buf);
close(cfd);
close(sfd);
remove(MY_SOCK_PATH);
return 0;
}
UNIX 域流式套接字客户端流程如下。
(1)创建 UNIX 域流式套接字。
(2)指定服务器端地址(套接字文件)。
(3)建立连接。
(4)发送/接收数据。
客户端代码如下:
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define MY_SOCK_PATH "/tmp/my_sock_file"
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_un peer_addr;
char buf[BUFSIZ] = {"Hello World!"};
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1)
handle_error("socket");
memset(&peer_addr, 0, sizeof(struct sockaddr_un));
peer_addr.sun_family = AF_UNIX;
strncpy(peer_addr.sun_path, MY_SOCK_PATH,
sizeof(peer_addr.sun_path) - 1);
if (connect(fd, (struct sockaddr *) &peer_addr,
sizeof(struct sockaddr_un)) == -1)
handle_error("connect");
printf("%s\n",buf);
send(fd, buf, strlen(buf), 0);
close(fd);
return 0;
}
2.UNIX 域数据报套接字
UNIX 域用户数据报套接字的流程可参考 UDP 套接字
UNIX 域流式套接字服务器端流程如下:
(1)创建 UNIX 域流式套接字。
(2)绑定本地地址(套接字文件)。
(3)发送/接收数据。
服务端代码如下:
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define MY_SOCK_PATH "/tmp/my_sock_file"
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_un my_addr, peer_addr;
socklen_t peer_addr_size;
char buf[BUFSIZ] = {};
fd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (fd == -1)
handle_error("socket");
memset(&my_addr, 0, sizeof(struct sockaddr_un));
my_addr.sun_family = AF_UNIX;
strncpy(my_addr.sun_path, MY_SOCK_PATH,
sizeof(my_addr.sun_path) - 1);
if (bind(fd, (struct sockaddr *) &my_addr,
sizeof(struct sockaddr_un)) == -1)
handle_error("bind");
peer_addr_size = sizeof(struct sockaddr_un);
recvfrom(fd, buf, BUFSIZ, 0, (struct sockaddr *) &peer_addr,
&peer_addr_size);
printf("%s\n",buf);
close(fd);
remove(MY_SOCK_PATH);
return 0;
}
客户端代码如下:
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define MY_SOCK_PATH "/tmp/my_sock_file"
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_un peer_addr;
socklen_t peer_addr_size;
char buf[BUFSIZ] = {"Hello World!"};
fd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (fd == -1)
handle_error("socket");
memset(&peer_addr, 0, sizeof(struct sockaddr_un));
peer_addr.sun_family = AF_UNIX;
strncpy(peer_addr.sun_path, MY_SOCK_PATH,
sizeof(peer_addr.sun_path) - 1);
peer_addr_size = sizeof(struct sockaddr_un);
printf("%s\n", buf);
sendto(fd, buf, strlen(buf), 0, (struct sockaddr *) &peer_addr,
peer_addr_size);
close(fd);
remove(MY_SOCK_PATH);
return 0;
}