1.网络和网络协议
1)什么是计算机网络?
计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过有形或无形的通信线路连接起来,在网络操作系统、网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
2)什么是网络协议?
网络协议是一种特殊的软件,是计算机网络实现其功能的最基本的机制。网路协议的本质就是规则,即各种硬件和软件必须遵循的共同守则。网络协议并不是一套单独的软件,它融合于所有涉及网络通信的软件甚至硬件之中,因此可以说协议于网络中无处不在。
3)什么是协议栈?
为了减少网络设计的复杂性,绝大多数网络采用分层设计的方法。所谓分层设计,就是按照信息流动的过程将网络的整体功能分解为一个个的功能层,不同机器上的同等功能层之间采用相同的协议,同一机器上相邻功能层之间通过接口进行信息传递。各层的协议和接口统称为协议栈。
ISO(Internation Stadardard Organization国际标准化组织)/
OSI(Open System Interconnection, 开放系统互联) 网络协议模型:
应用层:业务逻辑 \
表示层:数据的表现形式 | -> 应用层
会话层:建立、管理和终止通信过程 /
-------
传输层:源到目的地的点对点传输
网络层:路径选择、路由、寻址等网络结构拓扑
数据链路层:物理寻址、数据通道、错误检测等通信路径
物理层:在数据和电平信号之间进行转换
比喻:
买点心(应用层:业务需求)
选择包装(表示层:数据形式)
选择快递公司(会话层:通信过程)
填写收寄单(传输层:点对点)
选择路径(网络层:通路)
中途周转(数据链路层:多点连线)
送货过程(物理层:实际通信)
4)TCP/IP协议栈
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
链路层:ARP、RARP
5)消息包和数据流
应用层:HTTP请求=用户数据包
传输层:TCP头+用户数据包=TCP包
网络层:IP头+TCP包=IP包
链路层:以太网头+IP包+以太网尾=以太网帧
物理层:以太网帧->电平信号
| ^
v |
传输线路
发送数据流:消息自协议栈顶层向底层流动,逐层打包。
接收数据流:消息自协议栈底层向顶层流程,逐层解析。
6)IP地址
IP地址(Internet Protocol Address,互联网协议地址)是一种地址格式,为互联网上的每个网络和主机分配一个逻辑地址,其目的是消除物理地址的差异性。
IP地址的计算机内部用一个网络字节序的32位(4个字节)无符号整数表示。通常习惯将其表示为点分十进制整数字符串的形式。例如:
点分十进制整数字符串:1.2.3.4
32位(4个字节)无符号整数:0x01020304
内存布局:| 0x01 | 0x02 | 0x03 | 0x04 |
低地址--------------->高地址
网络字节序就是大端字节序,高位在低地址,低位在高地址。
中国北京市东城区珠市口大街珍贝大厦三层第一教室
一台计算机的IP地址=网络地址+主机地址
A级地址:以0为首8位网络地址+24位主机地址
B级地址:以10为首16位网络地址+16位主机地址
C级地址:以110为首24位网络地址+8位主机地址
D级地址:以1110为首的32为多(组)播地址
例如:某台计算机的IP地址为192.168.182.48,其网络地址和主机地址分别为何?
192 168 182 48
11000000 10101000 10110110 00110000
以110为首,C级地址,网络地址是192.168.182.0,主机地址是48。
主机IP地址 & 子网掩码 = 网络地址
主机IP地址 & ~子网掩码 = 主机地址
例如:主机IP地址192.168.182.48,子网掩码255.255.255.0,其网络地址和主机地址分别为何?
192.168.182.48 & 255.255.255.0 = 192.168.182.0
192.168.182.48 & 0.0.0.255 = 0.0.0.48
2.套接字
1)什么是套接字?
Socket,电源插座->套接字
一个由系统内核负责维护,通过文件描述符访问的对象, 可用于在同一台机器或不同机器中的进程之间实现通信。
进程表项
文件描述符表
0: 文件描述符标志 | * -> 标准输入文件表项 -> 键盘
1: 文件描述符标志 | * -> 标准输出文件表项 -> 显示器
2: 文件描述符标志 | * -> 标准错误文件表项 -> 显示器
3: 文件描述符标志 | * -> 套接字对象 -> 网卡
应用程序 应用程序
v v
磁盘文件的文件描述符 表示网络的文件描述符
v v
文件对象 套接字对象
v v
文件系统 网络协议栈
v v
磁盘设备 网络设备
套接字也可以被视为是围绕表示网络的文件描述符的一套函数库。调用其中的函数就可以访问网络上的数据,实现不同主机间的通信功能。
2)绑定和连接
套接字就是系统内核内存中的一块数据——逻辑对象
| 绑定(bind)
包含了IP地址和端口号等参数的网络应用——物理对象
互联网
| <-网络地址:192.168.182.0
子网络
| <-主机地址:0.0.0.48
计算机
| <-端口号:80
应用
通过IP地址(网络地址+主机地址)端口号就可以唯一定位互联网上的一个应用。
主机A
应用程序
|
逻辑对象(套接字)
| 绑定(bind) 连接(connection)
物理对象(IP地址和端口号)---------物理对象(IP地址和端口号)
| 绑定(bind)
逻辑对象(套接字)
|
应用程序
主机B
3)常用函数
创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
成功返回套接字描述符,失败返回-1。
domain - 通信域,即协议族,可取以下值:
PF_LOCAL/PF_UNIX: 本地通信,进程间通信
PF_INET: 互联网通信
PF_PACKET: 底层包通信(嗅探器、端口扫描)
type - 套接字类型,可取以下值:
SOCK_STREAM: 流式套接字,使用TCP协议
SOCK_DGRAM: 数据报文式套接字,使用UDP协议
SOCK_RAW: 原始套接字,使用自定义协议
protocol - 特殊协议
对于流式套接字和数据报式套接字,取0
套接字描述符与文件描述符在逻辑层面是一致的,所有关于文件描述符的规则对于套接字描述符也同样成立。同样也通过close函数关闭套接字,即释放内核中的有关资源。
基本地址结构:
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址值
};
基本地址结构仅用于给函数传参时做强制类型转换。
本地地址结构:
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // 地址族(AF_LOCAL/AF_UNIX)
char sun_path[]; // 套接字文件路径
};
网络地址结构:
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 地址族(AF_INET)
in_port_t sin_port; // 端口号(16字节网络字节序)
struct in_addr sin_addr; // 32字节IP地址
};
struct in_addr {
in_addr_t s_addr; // 网络字节序32位无符号整数形式的IP地址
};
typedef uint32_t in_addr_t;
typedef uint16_t in_port_t;
小端字节序的主机A 大端字节序的主机B
short a = 0x1234;
| 0x34 | 0x12 | | 0x34 | 0x12 |
低 高 低 高
a: 0x3412
发送:主机字节序->网络(大端)字节序
接收:网络(大端)字节序->主机字节序
小端字节序的主机A 大端字节序的主机B
0x1234
主:0x34 | 0x12
L H
网:0x12 | 0x34 -> 网:0x12 | 0x34
L H
主:0x12 | 0x34
0x1234
--------------------
小端字节序的主机C
-> 网:0x12 | 0x34
L H
主:0x34 | 0x12
0x1234
将套接字对象和自己的地址结构绑定在一起
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
成功返回0,失败返回-1。
sockfd - 套接字描述符
addr - 自己的地址结构
addrlen - 地址结构字节数
addr->sa_family: AF_LOCAL/AF_UNIX
((struct sockaddr_un*)addr)->sun_path: 套接字文件
addr->sa_family: AF_INET
((struct sockaddr_in*)addr)->sin_port/sin_addr:IP地址和端口号
将套接字对象所代表的物理对象和对方的地址结构连接在一起
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
成功返回0,失败返回-1。
sockfd - 套接字描述符
addr - 对方的地址结构
addrlen - 地址结构字节数
通过套接字描述符接收和发送数据的过程完全与通过文件描述符读取和写入数据的过程完全一样。
ssize_t read(int sockfd, void* buf, size_t count);
ssize_t write(int sockfd, const void* buf, size_t count);
字节序转换
通过网络传输多字节整数,需要在发送前转换为网络字节序,在接收后转换为主机字节序。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //主机到网络(32)
uint16_t htons(uint16_t hostshort); //主机到网络(16)
uint32_t ntohl(uint32_t netlong); //网络到主机(32)
uint16_t ntohs(uint16_t netshort); //网络到主机(16)
h - host,主机(字节序)
to - 到,把...转换到...
n - network,网络(字节序)
l - long版本,32位无符号整数
s - short版本,16位无符号整数
IP地址转换
(网络字节序32位无符号)整数<=>(点分十进制)字符串
#include <arpa/inet.h>
in_addr_t inet_addr(const char* cp); // 串->数
int inet_aton(const char* cp, struct in_addr* inp); // 串->数
输入参数 输出参数
转换成功返回0,失败返回-1。
char* inet_ntoa(struct in_addr in); // 数->串
转换成功返回字符串指针,失败返回NULL。
基于本地套接字的进程间通信:
服务器:提供业务服务的计算机程序。
客户机:请求业务服务的计算机程序。
服务器 客户机
创建套接字(socket) 创建套接字(socket)
准备地址结构(sockaddr_un) 准备地址结构(sockaddr_un)
绑定地址(bind) 建立连接(connect)
接收请求(read) 发送请求(write)
业务处理(...) 等待处理(...)
发送响应(write) 接收响应(read)
关闭套接字(close) 关闭套接字(close)
代码:locsvr.c、loccli.c
/* locsvr.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_FILE "mysock"
int main(void) {
printf("服务器:创建本地套接字\n");
int sockfd = socket(PF_LOCAL, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("服务器:准备地址并绑定\n");
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, SOCK_FILE);
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
return -1;
}
printf("服务器:接收数据\n");
for (;;) {
char buf[1024] = {};
ssize_t rb = read(sockfd, buf, sizeof(buf) - sizeof(buf[0]));
if (rb == -1) {
perror("read");
return -1;
}
if (!strcmp(buf, "!\n"))
break;
printf("< %s", buf);
}
printf("服务器:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("服务器:删除套接字文件\n");
if (unlink(SOCK_FILE) == -1) {
perror("unlink");
return -1;
}
printf("服务器:完成\n");
return 0;
}
/* loccli.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_FILE "mysock"
int main(void) {
printf("客户机:创建本地套接字\n");
int sockfd = socket(PF_LOCAL, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("客户机:准备地址并连接\n");
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, SOCK_FILE);
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect");
return -1;
}
printf("客户机:发送数据\n");
for (;;) {
printf("> ");
char buf[1024];
fgets(buf, sizeof(buf) / sizeof(buf[0]), stdin);
if (write(sockfd, buf, strlen(buf) * sizeof(buf[0])) == -1) {
perror("write");
return -1;
}
if (!strcmp(buf, "!\n"))
break;
}
printf("客户机:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("客户机:完成\n");
return 0;
}
基于网络套接字的进程间通信:
服务器 客户机
创建套接字(socket) 创建套接字(socket)
准备地址结构(sockaddr_in) 准备地址结构(sockaddr_in)
绑定地址(bind) 建立连接(connect)
接收请求(read) 发送请求(write)
业务处理(...) 等待处理(...)
发送响应(write) 接收响应(read)
关闭套接字(close) 关闭套接字(close)
代码:netsvr.c、netcli.c
/* netsvr.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "用法:%s <端口号>\n", argv[0]);
return -1;
}
printf("服务器:创建网络套接字\n");
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("服务器:准备地址并绑定\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
return -1;
}
printf("服务器:接收数据\n");
for (;;) {
char buf[1024] = {};
ssize_t rb = read(sockfd, buf, sizeof(buf) - sizeof(buf[0]));
if (rb == -1) {
perror("read");
return -1;
}
if (!strcmp(buf, "!\n"))
break;
printf("< %s", buf);
}
printf("服务器:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("服务器:完成\n");
return 0;
}
/* netcli.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char* argv[]) {
if (argc < 3) {
fprintf(stderr, "用法:%s <IP地址> <端口号>\n", argv[0]);
return -1;
}
printf("客户机:创建网络套接字\n");
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("客户机:准备地址并连接\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect");
return -1;
}
printf("客户机:发送数据\n");
for (;;) {
printf("> ");
char buf[1024];
fgets(buf, sizeof(buf) / sizeof(buf[0]), stdin);
if (write(sockfd, buf, strlen(buf) * sizeof(buf[0])) == -1) {
perror("write");
return -1;
}
if (!strcmp(buf, "!\n"))
break;
}
printf("客户机:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("客户机:完成\n");
return 0;
}