目录
一、网络发展历史
ARPANET阶段
TCP/IP两个协议阶段
网络体系结构--OSI开放系统互联模型
TCP/IP协议族体系结构
1.1 ARPANET阶段
ARPAnet,即“阿帕网”,是Interne的最早雏形
早期的ARPAnet使用网络控制协议(Network Control Protocol,NCP),
不能互联不同类型的计算机和不同类型的操作系统,没有纠错功能
1.2 TCP/IP两个协议阶段
TCP/IP协议分成了两个不同的协议:
用来检测网络传输中差错的传输控制协议TCP
专门负责对不同网络进行互联的互联网协议IP
1.3 OSI开放系统互联模型阶段
网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务
网络体系结构即指网络的层次结构和每层所使用协议的集合
OSI是由 ISO(国际标准化组织提出的)
共有七层:
从下往上说也行,从上往下说也可以,但是顺序不能颠倒。
物数网传会表应。

OSI模型相关的协议已经很少使用,但模型本身非常通用。
OSI模型是一个理想化的模型,尚未有完整的实现。

1.4 TCP/IP协议族的体系结构
由于OSI开放系统互联模型太过于繁琐,所以目前还没有完整的实现。
TCP/IP协议族体系结构相当于将OSI开放系统互联模型简化了。
虽然是简化了层次,但是要做的事情还是一样的。
TCP/IP协议族是一组协议,通常称它为TCP/IP协议族
共有四个层次:链路层、网络层、传输层、应用层。


每层常见的协议:
应用层:
HTTP(Hypertext Transfer Protocol) 超文本传输协议
万维网的数据通信的基础
FTP(File Transfer Protocol) 文件传输协议
是用于在网络上进行文件传输的一套标准协议,使用TCP传输
TFTP(Trivial File Transfer Protocol) 简单文件传输协议
是用于在网络上进行文件传输的一套标准协议,使用UDP传输
SMTP(Simple Mail Transfer Protocol) 简单邮件传输协议
一种提供可靠且有效的电子邮件传输的协议
传输层:
TCP(Transport Control Protocol) 传输控制协议
是一种面向连接的、可靠的、基于字节流的传输层通信协议
UDP(User Datagram Protocol) 用户数据报协议
是一种无连接、不可靠、快速传输的传输层通信协议
网络层:
IP(Internetworking Protocol) 网际互连协议
是指能够在多个不同网络间实现信息传输的协议
ICMP(Internet Control Message Protocol) 互联网控制信息协议
用于在IP主机、路由器之间传递控制消息,ping命令使用的就是该协议
IGMP(Internet Group Management Protocol) 互联网组管理
是一个组播协议,用于主机和组播路由器之间通信
链路层:
ARP(Address Resolution Protocol) 地址解析协议
通过IP地址获取对方mac地址
RARP(Reverse Address Resolution Protocol) 逆向地址解析协议
通过mac地址获取对方ip地址
注意:每层使用的协议,有下层决定,不能乱用

数据封包和拆包的过程


一帧数据说明:

有的书上写的是 64-1518 ,相当于将以太网头(14字节)和以太网尾(4字节)算进去了
1500是最大传输单元,linux系统默认的是 1500,使用 ifconfig 命令查看 MTU后面的数值即可
二、TCP和UDP的异同
共同点:同为传输层协议
不同点:
TCP:有连接,可靠
UDP:无连接,不保证可靠
TCP(即传输控制协议)概念:
是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数 据无失序、数据无重复到达的通信)
适用情况:
适合于对传输质量要求较高,以及传输大量数据的通信。
在需要可靠数据传输的场合,通常使用TCP协议
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
UDP(User Datagram Protocol)用户数据报协议
是不可靠的无连接的协议。
在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用情况:
发送小尺寸数据(如对DNS服务器进行IP地址查询时)
在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)
适合于广播/组播式通信中。
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输
三、网络基础知识
3.1 字节序
不同类型CPU的主机中,内存存储多字节整数序列有两种方法,称为主机字节序(HBO):
小端序(little-endian) - 低序字节存储在低地址
将低字节存储在起始地址,称为“Little-Endian”字节序,Intel、AMD等采用的是这种方式;
大端序(big-endian)- 高序字节存储在低地址
将高字节存储在起始地址,称为“Big-Endian”字节序,由ARM、Motorola等所采用
判断自己主机的字节序:
#include <stdio.h>
int main(){
int a = 0x12345678;
char *p = (char *)&a;
if(*p == 0x12){
printf("大端\n");
}else if(*p == 0x78){
printf("小端\n");
}
return 0;
}
如果多台主机在通信的过程中,如果字节序不一样,那么就有可能导致收到的数据和发送的不一致的情况,所以为了保证网络通信的过程中数据的一致性,就发明了 网络字节序(大端序),意味着发送方发送数据时,需要转换成网络字节序发送,接收方接到的数据就是网络字节序的数据,需要转换成主机字节序再处理。
什么样的数据收发的时候需要考虑字节序?
明确知道通信的双发的主机字节序一样时,可以不用考虑
将超过一个字节的数据作为整体收发的时候,需要考虑字节序(字符串不需要考虑)
字节序转换的方式,以4字节 主机字节序转网络字节序 为例:
#include <stdio.h>
int main(){
int num;
scanf("%d", &num);
int test_num = 0x12345678;
char *test = (char *)&test_num;
if(*test == 0x12){
num = num;
}else if(*test == 0x78){
char *p = (char *)#
char *q = p+3;
char temp = *p;
*p = *q;
*q = temp;
p++;
q--;
temp = *p;
*p = *q;
*q = temp;
}
return 0;
}
字节序转换的函数:
h host 主机 to 转换 n network 网络 l int s short
uint32_t htonl(uint32_t hostlong); //主机字节序转网络字节序 4字节
uint16_t htons(uint16_t hostshort); //主机字节序转网络字节序 2字节
uint32_t ntohl(uint32_t netlong); //网络字节序转主机字节序 4字节
uint16_t ntohs(uint16_t netshort); //网络字节序转主机字节序 2字节
例如:
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, const char *argv[])
{
unsigned int num = 0x12345678;
unsigned int ret = htonl(num);
printf("%#x --> %#x\n", num, ret);
return 0;
}
执行结果:

3.2 socket
linux内核的五大功能:
进程管理 ----时间片轮转、上下文切换
文件管理 ----将一堆 0 和 1 转换方便人类识别的字符
设备管理 ----linux下一切皆文件
内存管理 ----内存的分配和回收
网络管理 ----网络协议栈的管理
socket是一个系统调用接口,会返回一个文件描述符,用户想通过网络收发数据的时候,只需要对该文件描述符进行读写即可,数据使用什么协议发送,通过调用socket函数时传参的方式来决定,相当于把复杂的网络通信的过程,转换成了我们熟悉的文件io操作。
socket并不仅限于TCP/IP协议族体系结构,其他的体系结构也会使用socket
socket也并不仅限于linux
socket的类型:
流式套接字(SOCK_STREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接 收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。 ----TCP
数据报套接字(SOCK_DGRAM)
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失 或重复,顺序发送,可能乱序接收。----UDP
原始套接字(SOCK_RAW)
可以对较低层次协议如IP、ICMP直接访问。
3.3 IP地址
MAC地址是网卡的物理地址,理论上讲应该是全球唯一的,
但是现在有很多虚拟化的技术,已经不保证全球唯一了,但是同一局域网中一定唯一。
MAC地址占用 6个字节,例如 00:0c:29:a1:f9:68
linux中使用 ifconfig 命令可以查看mac地址
win中使用 ipconfig/all 命令可以查看mac地址
局域网内部使用 MAC地址通信,如果数据想走出局域网,就需要用到IP地址了。
IP地址的分类
IPV4 4字节 32bit
IPV6 16字节 128bit
现在企业中大部分使用的还是IPV4,因为有NAT技术的加持,
能很大程度缓解IP地址不够用的问题。
IP地址的表示形式 "192.168.80.10" 这种叫做点分十进制,是一个字符串
计算机中存储IP地址是用的无符号4字节整型。unsigned int
IPV4地址的组成:
由 网络号 和 主机号 组成
IPV4地址分类:
网络号 主机号 规定最高位 范围 使用单位
A 1字节 3字节 0 0.0.0.0 - 127.255.255.255 政府/大公司/学校
B 2字节 2字节 10 128.0.0.0-191.255.255.255 中等规模的公司
C 3字节 1字节 110 192.0.0.0-223.255.255.255 个人
192.168.1.255 广播地址
D 1110 [224-239] 组播
E 11110 [240-255] 保留测试用的
其中每个IP地址又可以通过路由器,下发局域网IP地址,
每类IP地址都有专门划分子网的保留段。

子网掩码:
是由一堆连续的1和连续的0组成的。
用来和IP地址取与运算来获取网络号的。
从而能限制某一网段内能容纳的最大主机数。
如,ip地址是192.168.70.8 子网掩码设置成 255.255.255.0
取与运算可以得到的结果:192.168.70.0 ----这是网络号,网络号相同时,才能进行同信
这时,该网段内共有IP地址 256 个:
其中 192.168.70.0 是网络号,是不能占用的
192.168.70.255 是广播的地址,也不能占用
网关设备也需要占用一个IP地址,一般是同一局域网内可用的编号最小的。
192.168.70.1
所以能容纳的主机数:256-1-1-1(网关设备也可以算作一台IP主机) = 254
192.168.70.x 网段将子网掩码设置成 255.255.255.128,同一子网能容纳的最大主机数?
答案:
这种方式可以将同一网段再划分成两个子网
第一个子网IP地址范围:192.168.70.0~192.168.70.127
第二个子网IP地址范围:192.168.70.128~192.168.70.255
其中每个子网中有需要减掉 网络号 广播地址 网关地址
所以子网掩码不一定都是255255255.0 网关设备的IP地址也不一定是编号最小的。
既然是无符号4字节整型,就涉及字节序的问题。
在网络字节序下,ip地址的存储:

//将点分十进制的字符串 转换成 网络字节序的 无符号四字节整型
in_addr_t inet_addr(const char *cp);
//将网络字节序的无符号四字节整型的ip地址 转换成 点分十进制的字符串
char *inet_ntoa(struct in_addr in);
例如:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, const char *argv[])
{
char str[] = "192.168.80.30";
unsigned int ip = inet_addr(str);
unsigned char *p = (unsigned char *)&ip;
printf("%s --> %d.%d.%d.%d\n", str, *p, *(p+1), *(p+2), *(p+3));
return 0;
}
3.4 端口号
网络通信是不通主机的进程间通信,通过IP地址可以在复杂的网络环境中找到对应的主机
但是还得确定消息发给哪个进程。
如果使用pid来标识进程
第一个问题pid没法手动指定
第二个问题是进程重启时一般pid都会发生变化,不好管理
所以就发明了端口号,用来人为的标识某一个进程。
端口号一般由IANA (Internet Assigned Numbers Authority) 管理
众所周知端口:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用)
已登记端口:1024~49151
动态或私有端口:49152~65535
linux系统端口号的范围 [0-65535] 共计 65536个 使用 unsigned short 存储
实际开发的时候,端口号是由用户指定的,学习阶段我们为了防止冲突
可以使用一些特殊数字的,如 8888 9999 6789 6666 等
linux系统中 /etc/services 中保存的就是当前系统中已经被占用的端口号
常见的服务使用的端口号:
ftp 21
ssh 22
tftp 69
http 80 8080
四、TCP网络编程
4.1 网络编程模型
C/S模型 客户端/服务器模型
B/S模型 浏览器/服务器
4.2 TCP网络编程流程

服务器流程:
创建套接字
填充服务器的网络信息结构体
将套接字和服务器的网络信息结构体绑定
把套接字设置成被动监听状态
阻塞等待客户端连接
收发数据
关闭套接字
客户端流程:
创建套接字
填充服务器的网络信息结构体
与服务器建立连接
收发数据
关闭套接字
4.3 函数说明
4.3.1 socket函数
功能:
创建套接字
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int socket(int domain, int type, int protocol);
参数:
domain:
通信域
AF_UNIX, AF_LOCAL 本地通信使用
AF_INET IPV4使用
AF_INET6 IPV6使用
AF_PACKET 原始套接字使用
type:
套接字类型
SOCK_STREAM TCP使用
SOCK_DGRAM UDP使用
SOCK_RAW 原始套接字使用
protocol:
附加协议,如果没有 传0即可
返回值:
成功 文件描述符
失败 -1
4.3.2 bind函数
功能:
绑定套接字和网络信息结构体
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字
addr: 网络信息结构体
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
上面的结构体只是为了防止编译警告 做强转用的
我们用的是下面的结构体
struct sockaddr_in {
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 网络字节序的端口号 */
struct in_addr sin_addr; /* 网络地址 */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* 网络字节序无符号4字节整型的IP地址 */
};
addrlen:addr
返回值:
成功 0
失败 -1
4.3.3 listen函数
功能:
将套接字设置成被动监听状态
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int listen(int sockfd, int backlog);
参数:
sockfd:套接字
backlog:半监听队列的长度
一般设置成 5 10 都可以 只要不是0 就行
返回值:
成功 0
失败 -1
4.3.4 accept函数
功能:
阻塞等待客户端连接
头文件:
#include <sys/socket.h>
函数原型:
int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
参数:
socket:已经设置成listen状态的套接字
address:用来保存客户端网络信息结构体的缓冲区的首地址
如果不想保存,可以设置成 NULL
address_len:address 如果不需要 也可以传 NULL
返回值:
成功 返回一个新的文件描述符专门用于和该客户端通信
失败 -1
4.3.5 connect函数
功能:
与服务器建立连接
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字
addr: 服务器的网络信息结构体
addrlen:addr大小
返回值:
成功 0
失败 -1
4.4 代码实现
4.4.1 服务器代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, const char *argv[])
{
//创建套接字 --买了一部手机
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
perror("socket error");
exit(-1);
}
//填充服务器网络信息结构体 --买卡
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
//端口号可以自己指定 一般使用 8888 9999 等 都可以
serveraddr.sin_port = htons(8889);
//ip地址不能乱填 自己的Ubuntuip地址是多少就填多少
//如果是自己的主机测试,不走网络的话 可以使用 127.0.0.1
serveraddr.sin_addr.s_addr = inet_addr("192.168.80.58");
socklen_t serveraddr_len = sizeof(serveraddr);
//将套接字和服务器网络信息结构体绑定 --插卡
if(-1 == bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
perror("bind error");
exit(-1);
}
//将套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
perror("listen error");
exit(-1);
}
//阻塞等待客户端连接
//如果成功了, 对acceptfd的操作 就是对当前连接上的客户端的操作
int acceptfd = accept(sockfd, NULL, NULL);
if(-1 == acceptfd){
perror("accept error");
exit(-1);
}
printf("有客户端连接到服务器..\n");
char buff[128] = {0};
//接收数据
read(acceptfd, buff, 128);
printf("客户端发来数据[%s]\n", buff);
//组装应答消息
strcat(buff, "--hqyj");
//发送应答
write(acceptfd, buff, 128);
//关闭套接字
close(acceptfd);
close(sockfd);
return 0;
}
4.4.2 客户端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, const char *argv[])
{
//创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
perror("socket error");
exit(-1);
}
//填充服务器网络信息结构体 --用来指定连接哪个服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
//端口号可以自己指定 一般使用 8888 9999 等 都可以
serveraddr.sin_port = htons(8889);
//ip地址不能乱填 自己的Ubuntuip地址是多少就填多少
//如果是自己的主机测试,不走网络的话 可以使用 127.0.0.1
serveraddr.sin_addr.s_addr = inet_addr("192.168.80.58");
socklen_t serveraddr_len = sizeof(serveraddr);
//与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
perror("connect error");
exit(-1);
}
printf("与服务器连接成功..\n");
char buff[128] = {0};
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';
//发送数据
write(sockfd, buff, 128);
//接收应答消息
read(sockfd, buff, 128);
printf("应答消息[%s]\n", buff);
//关闭套接字
close(sockfd);
return 0;
}

83

被折叠的 条评论
为什么被折叠?



