目录
学习目标
- 了解OSI七层、TCP/IP四层模型结构
- 了解常见网络协议格式
- 掌握网络字节序和主机字节序之间的转换(大端法和小端法)
- 说出TCP服务器端通信流程
- 说出TCP客户端通信流程
- 独立写出TCP服务器端代码
- 独立写出TCP客户端代码
1 网络基础概念
1.1 协议
概念: 协议事先约定好, 大家共同遵守的一组规则, 如交通信号灯.
从应用程序的角度看, 协议可理解为数据传输和数据解释的规则;
可以简单的理解为各个主机之间进行通信所使用的共同语言.
假设,A、B双方欲传输文件。规定:
第一次: 传输文件名,接收方接收到文件名,应答OK给传输方;
第二次: 发送文件的尺寸,接收方接收到该数据再次应答一个OK;
第三次: 传输文件内容。同样,接收方接收数据完成后应答OK表示文件内容接收成功。
这种在A和B之间被遵守的协议称之为原始协议, 后来经过不断增加完善改进, 最终形成了一个稳定的完整的传输协议, 被广泛应用于各种文件传输, 该协议逐渐就成了一个标准协议.
几种常见的协议
传输层 常见协议有TCP/UDP协议。
应用层 常见的协议有HTTP协议,FTP协议。
网络层 常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层 常见协议有ARP协议、RARP协议。
TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
FTP文件传输协议(File Transfer Protocol)
IP协议是因特网互联协议(Internet Protocol)
ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
RARP是反向地址转换协议,通过MAC地址确定IP地址。
1.2分层模型
OSI是Open System Interconnection的缩写, 意为开放式系统互联. 国际标准化组织(ISO)制定了OSI模型, 该模型定义了不同计算机互联的标准, 是设计和描述计算机网络通信的基本框架.
网络分层 OSI 7层模型: 物数网传会表应
- 物理层---双绞线,光纤(传输介质),将模拟信号转换(高低电平)为数字信号(0、1)
- 数据链路层---数据校验,定义了网络传输的基本单位-帧(网络报文格式)
- 网络层---定义网络,两台机器之间传输的路径选择点到点的传输 ip协议 路由器
- 传输层---传输数据 TCP,UDP,端到端的传输 ARP(通过ip找到主机mac)、RARP协议(通过主机mac找到ip)
- 会话层---通过传输层建立数据传输的通道.
- 表示层---编解码,翻译工作.
应用层---为客户提供各种应用服务,email服务,ftp服务,ssh服务
1.3 数据通信过程
通信过程: 其实就是发送端层层打包, 接收方层层解包.
注意: 这些操作不是用户自己做的, 而是底层帮我们做好的.
1.4 网络应用程序的设计模式
- CS设计模式优缺点:
优点:
客户端在本机上可以保证性能, 可以将数据缓存到本地, 提高数据的传输效率, 提高用户体验效果.
客户端和服务端程序都是由同一个开发团队开发, 协议选择比较灵活.
缺点:
服务器和客户端都需要开发,工作量相对较大, 调试困难, 开发周期长;
从用户的角度看, 需要将客户端安装到用户的主机上, 对用户主机的安 全构成威胁.
- BS设计模式优缺点:
优点:
无需安装客户端, 可以使用标准的浏览器作为客户端;
只需要开发服务器,工作量相对较小;
由于采用标准的客户端, 所以移植性好, 不受平台限制.
相对安全,不用安装软件
缺点:
由于没有客户端, 数据缓冲不尽人意, 数据传输有限制, 用户体验较差;
通信协议选择只能使用HTTP协议,协议选择不够灵活;
1.5 以太网帧格式
以太网帧格式就是包装在网络接口层(数据链路层)的协议
Mac地址占6个字节,IP地址占4个字节
以ARP为例介绍以太网帧格式
目的端mac地址是通过发送端发送ARP广播, 接收到该ARP数据的主机先判断是否是自己的IP, 若是则应答一个ARP应答报文, 并将mac地址填入应答报文中; 若目的IP不是自己的主机IP, 则直接丢弃该ARP请求报文.
详细讲解ARP协议
IP段格式
协议版本: ipv4, ipv6
16位总长度: 最大65536
8位生存时间ttl(网络连接下一跳的次数): 为了防止网络阻塞(中间节点个数大于ttl数则会丢包,防止找不到导致网络阻塞)
32位源ip地址, 共个4字节!我们熟悉的ip都是点分十进制的,4字节, 每字节对应一个点分位,最大为255 ,实际上就是整形数!
32位目的ip地址
8位协议: 用来区分上层协议是TCP, UDP, ICMP还是IGMP协议.
16位首部校验和: 只校验IP首部, 数据的校验由更高层协议负责.
UDP数据报格式
通过IP地址来确定网络环境中的唯一的一台主机;
16位源端口号 最大 2的16次方 – 1 65535
主机上使用端口号来区分不同的应用程序. Ssh hhtp等都是不同的端口号
在etc目录下的services可以看到端口号
IP+端口唯一确定唯一一台主机上的一个应用程序.
TCP数据流格式:
ACK是确认,你给对面发送的时候,对方回复你的时候才有ACK,直接发送的时候没有ACK
SYN指的是在建立请求的时候会有这个请求标识
FIN指的是finish,在关闭的时候会有
32位序号代表唯一的序号,采用多线程处理数据时可根据序号顺序进行处理。
稳定的, 安全的, 可靠的
序号: TCP是安全可靠的, 每个数据包都带有序号, 当数据包丢失的时候, 需要重传, 要使用序号进行重传. 控制数据有序, 丢包重传.
确认序号: 使用确认序号可以知道对方是否已经收到了, 通过确认序号可以知道哪个序号的数据需要重传.
16位窗口大小--滑动窗口(主要进行流量控制)
2 SOCKET编程
传统的进程间通信借助内核提供的IPC机制进行, 但是只能限于本机通信, 若要跨机通信, 就必须使用网络通信.( 本质上借助内核-内核提供了socket伪文件的机制实现通信----实际上是使用文件描述符), 这就需要用到内核提供给用户的socket API函数库.socket中文也叫插座
既然提到socket伪文件, 所以可以使用文件描述符相关的函数read write
可以对比pipe管道讲述socket文件描述符的区别.
使用socket会建立一个socket pair.
如下图, 一个文件描述符操作两个缓冲区, 这点跟管道是不同的, 管道是两个文件描述符操作一个内核缓冲区.
2.1 socket编程预备知识
网络字节序:
大端和小端的概念
大端: 低位地址存放高位数据, 高位地址存放低位数据
小端: 低位地址存放低位数据, 高位地址存放高位数据
大端和小端的使用使用场合???
大端和小端只是对数据类型长度是两个及以上的, 如int short, 对于单字节 没限制, 在网络中经常需要考虑大端和小端的是IP和端口.
思考题: 0x12345678如何存放???
如何验证本机上大端还是小端??-----使用共用体.共同体的特点是共同体内的数据共享同一片地址,也就是当一个共同体里面有结构体和无符号整形时,联合体内部的结构体的首地址和无符号整形数据的首地址是相同的
编写代码endian.c进行测试, 测试本机上是大端模式还是小端模式?(小端模式)
#include <stdio.h>
#include <stdlib.h>
union {
short s;
char c[sizeof(short)];
} un2;
union {
int s;
char c[sizeof(int)];
}un4;
int main()
{
printf("[%d][%d][%d]\n", sizeof(short), sizeof(int), sizeof(long int));
//测试short类型
un2.s = 0x0102;// 0x0102 =? 16*16+2 低字节存放低位数据 高字节存放高位数据 属于小端(低端)模式
printf("%d,%d,%d\n",un2.c[0],un2.c[1],un2.s);
//测试int类型
//un4.s = 0x12345678;
un4.s = 0x01020304;
printf("%d,%d,%d,%d,%d\n", un4.c[0], un4.c[1], un4.c[2], un4.c[3], un4.s);
return 0;
}
网络传输用的是大端法, 如果机器用的是小端法, 则需要进行大小端的转换.
下面4个函数就是进行大小端转换的函数:
#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);
函数名的h表示主机host, n表示网络network, s表示short, l表示long
上述的几个函数, 如果本来不需要转换函数内部就不会做转换.
P地址转换函数:
p->表示点分十进制的字符串形式
to->到
n->表示network网络
int inet_pton(int af, const char *src, void *dst);
/*
函数说明: 将字符串形式的点分十进制IP转换为大端模式的网络IP(整形4字节数)
参数说明:
af: AF_INET
src: 字符串形式的点分十进制的IP地址
dst: 存放转换后的变量的地址
例如: inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
*/
手工也可以计算: 如192.168.232.145, 先将4个正数分别转换为16进制数,
192--->0xC0 168--->0xA8 232--->0xE8 145--->0x91
最后按照大端字节序存放: 0x91E8A8C0, 这个就是4字节的整形值.
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
/*
函数说明: 网络IP转换为字符串形式的点分十进制的IP
参数说明:
af: AF_INET
src: 网络的整形的IP地址
dst: 转换后的IP地址,一般为字符串数组
size: dst的长度
返回值:
成功--返回指向dst的指针
失败--返回NULL, 并设置errno
*/
例如: IP地址为010aa8c0, 转换为点分十进制的格式:
01---->1 0a---->10 a8---->168 c0---->192
由于从网络中的IP地址是高端模式, 所以转换为点分十进制后应该为: 192.168.10.1
socket编程用到的重要的结构体:struct sockaddr
struct sockaddr结构说明:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
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 */
}; //网络字节序IP--大端模式
//通过man 7 ip可以查看相关说明
2.2 socket编程主要的API函数介绍
int socket(int domain, int type, int protocol);
/*
函数描述: 创建socket
参数说明:
domain: 协议版本
AF_INET IPV4
AF_INET6 IPV6
AF_UNIX AF_LOCAL本地套接字使用
type:协议类型
SOCK_STREAM 流式, 默认使用的协议是TCP协议
SOCK_DGRAM 报式, 默认使用的是UDP协议
protocal:
一般填0, 表示使用对应类型的默认协议.
返回值:
成功: 返回一个大于0的文件描述符
失败: 返回-1, 并设置errno
*/
当调用socket函数以后, 返回一个文件描述符, 内核会提供与该文件描述符相对应的读和写缓冲区, 同时还有两个队列, 分别是请求连接队列和已连接队列.
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
函数描述: 将socket文件描述符和IP,PORT绑定
参数说明:
socket: 调用socket函数返回的文件描述符
addr: 本地服务器的IP地址和PORT,
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
//serv.sin_addr.s_addr = htonl(INADDR_ANY);
//INADDR_ANY: 表示使用本机任意有效的可用IP
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
addrlen: addr变量的占用的内存大小
返回值:
成功: 返回0
失败: 返回-1, 并设置errno
*/
int listen(int sockfd, int backlog);
/*
函数描述: 将套接字由主动态变为被动态
参数说明:
sockfd: 调用socket函数返回的文件描述符
backlog: 同时请求连接的最大个数(还未建立连接)
返回值:
成功: 返回0
失败: 返回-1, 并设置errno
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
函数说明:获得一个连接, 若当前没有连接则会阻塞等待.
函数参数:
sockfd: 调用socket函数返回的文件描述符
addr: 传出参数, 保存客户端的地址信息
addrlen: 传入传出参数, addr变量所占内存空间大小
返回值:
成功: 返回一个新的文件描述符,用于和客户端通信
失败: 返回-1, 并设置errno值.
*/
accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
从已连接队列中获取一个新的连接, 并获得一个新的文件描述符, 该文件描述符用于和客户端通信. (内核会负责将请求队列中的连接拿到已连接队列中)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
函数说明: 连接服务器
函数参数:
sockfd: 调用socket函数返回的文件描述符
addr: 服务端的地址信息
addrlen: addr变量的内存大小
返回值:
成功: 返回0
失败: 返回-1, 并设置errno值
*/
接下来就可以使用write和read函数进行读写操作了.
除了使用read/write函数以外, 还可以使用recv和send函数
//读取数据和发送数据:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//对应recv和send这两个函数flags直接填0就可以了.
注意: 如果写缓冲区已满, write也会阻塞, read读操作的时候, 若读缓冲区没有数据会引起阻塞.
使用socket的API函数编写服务端和客户端程序的步骤图示:
根据服务端和客户端编写代码的流程, 编写代码并进行测试.
//服务器端程序
//服务器端开发流程
/*
1 创建socket,返回一个文件描述符lfd---socket()
--该文件描述符用于监听客户端连接
2 将lfd和IP PORT进行绑定----bind()
3 将lfd由主动变为被动监听----listen()
4 接受一个新的连接,得到一个文件描述符cfd----accept()
---该文件描述符是用于和客户端进行通信的
5 while(1)
{
接收数据---read或者recv
发送数据---write或者send
}
6 关闭文件描述符----close(lfd) close(cfd);
nc IP地址 端口号可进行简单的测试 如nc 127.0.0.1 8888
*/
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
int main()
{
//第一步 创建socket
//int socket(int domain, int type, int protocol);
//参数1 AF_INET 代表ipv4地址
//参数2 SOCK_STREAM TCP SOCK_DGRAM UDP
//参数3 默认为0
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd < 0)
{
perror("create socket error");
}
//第二步 绑定
// int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888); //host代表主机 s短整型 l长整型
serv.sin_addr.s_addr = htonl(INADDR_ANY); //表示使用本地任意可用IP
int ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
if (ret < 0)
{
perror("bind error\n");
}
//第三步 监听 128代表最多端口号
listen(lfd, 128);
//第四步 接收连接
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//accept之前可能就已经创建了连接 通过netstat anp | grep 8888可进行查看连接状态
//第一种写法
// int cfd = accept(lfd, NULL, NULL);
//第二种写法 获取客户端的ip地址和端口号
struct sockaddr_in server;
socklen_t len = sizeof(server);
//最后一个参数为传入传出参数 addr变量所占内存空间大小
int cfd = accept(lfd, (struct sockaddr *)&server, &len);
//获取的为网络的大端模式下的数据 需要进行类型转换
//获取客户端ip地址和端口号
char sIP[16];
memset(sIP, 0x00, sizeof(sIP));
//const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
printf("client IP = [%s] Port : [%d]\n", inet_ntop(AF_INET, &server.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(server.sin_port));
printf("lfd = [%d], cfd = [%d]\n", lfd, cfd);
int n = 0;
int i = 0;
char buf[1024];
//接受数据
while (1)
{
//读取数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n<=0)
{
printf("read error or client close\n");
break;
}
printf("buf = [%s], length = [%d]\n", buf, n);
//发送数据
for (i = 0; i < n; i++)
{
//小写转大写
buf[i] = toupper(buf[i]);
}
write(cfd, buf, n);
}
//关闭文件描述符
close(lfd);
close(cfd);
return 0;
}
使用nc ip地址 端口号可作客户端进行测试
客户端代码如下
//客户端程序开发
/*
客户端的开发流程:
1 创建socket, 返回一个文件描述符cfd---socket()
---该文件描述符是用于和服务端通信
2 连接服务端---connect()
3 while(1)
{
//发送数据---write或者send
//接收数据---read或者recv
}
4 close(cfd)
*/
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
int main()
{
//第一步 创建socket int socket(int domain, int type, int protocol);
//用于和服务端进行通信
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if(cfd < 0)
{
perror("create socket error\n");
return -1;
}
//第二步 连接客户端 int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888); //host代表主机 s短整型 l长整型
//int inet_pton(int af, const char *src, void *dst);
//将字符串形式的点分十进制IP转换为大端模式的网络IP(整形4字节数)
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
printf("[%x]\n", serv.sin_addr.s_addr);
int ret = connect(cfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret < 0)
{
perror("connect error\n");
return -1;
}
int n = 0;
char buf[256];
while (1)
{
//发送数据
memset(buf, 0x00, sizeof(buf));
n = read(STDIN_FILENO, buf, sizeof(buf)); //从输入终端获取数据
//写数据
write(cfd, buf, n);
//读取服务器端数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n <= 0)
{
printf("read error or server close!\n");
break;
}
printf("n == [%d], buf = [%s]\n", n, buf);
}
//关闭文件描述符
close(cfd);
return 0;
}
测试过程中可以使用netstat命令查看监听状态和连接状态
netstat命令:
a表示显示所有,
n表示显示的时候以数字的方式来显示
p表示显示进程信息(进程名和进程PID)
作业:
自己编写代码熟悉一下服务端和客户端的代码开发流程;
设计服务端和客户端通信协议(属于业务层的协议)
如发送结构体
typedef struct teacher_{
int tid;
char name[30];
int age;
char sex[30];
int sal;
} teacher;
typedef struct student_{
int sid;
char name[30];
int age;
char sex[30];
}student;
typedef struct SendMsg_{
int type;//1 - teacher;2 - student
int len;//
char buf[0];//变长发送数据
}SendMsg;