1.网络的分层结构
因为网络是一种非常复杂的通信方式,所以要通过分层来进行开发难度的降低。因此我们在研究网络通信时,一定要在同一个层次进行研究,不能跨层次研究,比如分析客户端和服务器的收发时,要分析API层次时,两部分都要统一在这个层次进行分析,而不能是一端分析API接口,另一端却去分析驱动了。一般情况下,我们在网络编程时最关注的是应用层,传输层只需要了解即可。
2.BS和CS
(1)CS架构介绍(client server,客户端服务器架构)。比如QQ,360网盘之类的(在电脑上或手机上用软件登录的)。
(2)BS架构介绍(broswer server,浏览器服务器架构)。比如在线版的QQ,网页版360网盘(用浏览器打开的)。
TCP协议
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
1、关于TCP理解的重点
2、TCP如何保证可靠传输
建立TCP需要三次握手才能建立,而断开连接则需要四次握手。整个过程如下图所示:
3、TCP的三次握手
4、TCP的四次挥手
注:这些握手协议已经封装在TCP协议内部,socket编程接口平时不用管
5、基于TCP通信的服务模式
1、建立连接
2、发送和接收
3、辅助性函数
4、表示IP地址相关数据结构
都定义在 netinet/in.h
struct sockaddr,这个结构体是网络编程接口中用来表示一个IP地址的,注意这个IP地址是兼容IPv4和IPv6的
typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型
struct in_addrstruct in_addr { in_addr_t s_addr; };
struct sockaddr_in
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
在进行格式转换时,这些函数,默认都使用大端模式
#include
#include
#include
#include
#define IPADDR "192.168.1.102"
// 0x66 01 a8 c0
// 102 1 168 192
// 网络字节序,其实就是大端模式
int main(void)
{
//使用inet_ntop来转换
struct in_addr addr = {0};
char buf[50] = {0};
addr.s_addr = 0x6601a8c0;
inet_ntop(AF_INET, &addr, buf, sizeof(buf));
printf("ip addr = %s.\n", buf); //192.168.1.102
#if 0
// 使用inet_pton来转换
int ret = 0;
struct in_addr addr = {0};
ret = inet_pton(AF_INET, IPADDR, &addr);
if (ret != 1)
{
printf("inet_pton error\n");
return -1;
}
printf("addr = 0x%x.\n", addr.s_addr); //0x6601a8c0
#endif
#if 0 //使用inet_addr来转换
in_addr_t addr = 0;
addr = inet_addr(IPADDR);
printf("addr = 0x%x.\n", addr); // 0x6601a8c0
#endif
return 0;
}
概念:端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程。端口号和IP地址一起会被打包到当前进程发出或者接收到的每一个数据包中。每一个数据包将来在网络上传递的时候,内部都包含了发送方和接收方的信息(就是IP地址和端口号),所以IP地址和端口号这两个往往是打包在一起不分家的。
探讨:如何让服务器和客户端好好沟通
(1)客户端和服务器原则上都可以任意的发和收,但是实际上双方必须配合:client发的时候server就收,而server发的时候client就收
(2)必须了解到的一点:client和server之间的通信是异步的,这就是问题的根源
(3)解决方案:依靠应用层协议来解决。说白了就是我们server和client事先做好一系列的通信约定。
自定义应用层协议
1、自定义应用层协议第一步:规定发送和接收方法
(1)规定连接建立后由客户端主动向服务器发出1个请求数据包,然后服务器收到数据包后回复客户端一个回应数据包,这就是一个通信回合
(2)整个连接的通信就是由N多个回合组成的。
2、自定义应用层协议第二步:定义数据包格式
3、常用应用层协议:http、ftp······
下面以一个例子展示网络编程:客户端向服务器注册学生的基本信息(发送一个数据包),服务器回应一个数据包表示接收完成(展示学生的基本信息)。
服务器端代码:server.c
#include
#include
#include /* See NOTES */
#include
#include
#include
#define SERPORT 9003
#define SERADDR "192.168.1.104" // ifconfig看到的
#define BACKLOG 100 //貌似是最大能容纳的连接数
char recvbuf[100];
#define CMD_REGISTER 1001 // 注册学生信息
#define CMD_CHECK 1002 // 检验学生信息
#define CMD_GETINFO 1003 // 获取学生信息
#define STAT_OK 30 // 回复ok
#define STAT_ERR 31 // 回复出错了
typedef struct commu
{
char name[20]; // 学生姓名
int age; // 学生年龄
int cmd; // 命令码
int stat; // 状态信息,用来回复
}info;
int main(void)
{
int sockfd = -1, ret = -1, clifd = -1;
socklen_t len = 0;
//这里的结构为sockaddr_in结构体包含sin_port和sin_addr结构体,sin_addr结构体包含s_addr
struct sockaddr_in seraddr = {0};
struct sockaddr_in cliaddr = {0};
// 第1步:先socket打开文件描述符
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return -1;
}
printf("socketfd = %d.\n", sockfd);
// 第2步:bind绑定sockefd和当前电脑的ip地址&端口号
seraddr.sin_family = AF_INET; // 设置地址族为IPv4
seraddr.sin_port = htons(SERPORT); // 设置地址的端口号信息
seraddr.sin_addr.s_addr = inet_addr(SERADDR); // 设置IP地址
ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret < 0)
{
perror("bind");
return -1;
}
printf("bind success.\n");
// 第3步:listen监听端口
ret = listen(sockfd, BACKLOG); // 阻塞等待客户端来连接服务器
if (ret < 0)
{
perror("listen");
return -1;
}
// 第4步:accept阻塞等待客户端接入
clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
printf("连接已经建立,client fd = %d.\n", clifd);
// 客户端反复给服务器发
while (1)
{
info st;
// 回合中第1步:服务器收
ret = recv(clifd, &st, sizeof(info), 0);
// 回合中第2步:服务器解析客户端数据包,然后干活,
if (st.cmd == CMD_REGISTER)
{
printf("用户要注册学生信息\n");
printf("学生姓名:%s,学生年龄:%d\n", st.name, st.age);
// 在这里服务器要进行真正的注册动作,一般是插入数据库一条信息
// 回合中第3步:回复客户端
st.stat = STAT_OK;
ret = send(clifd, &st, sizeof(info), 0);
}
}
return 0;
}
客户端代码:client.c
#include
#include
#include /* See NOTES */
#include
#include
#include
#define SERADDR "192.168.1.104" // 服务器开放给我们的IP地址和端口号
#define SERPORT 9003
//发送、接收缓冲区
char sendbuf[100];
char recvbuf[100];
#define CMD_REGISTER 1001 // 注册学生信息
#define CMD_CHECK 1002 // 检验学生信息
#define CMD_GETINFO 1003 // 获取学生信息
#define STAT_OK 30 // 回复ok
#define STAT_ERR 31 // 回复出错了
typedef struct commu
{
char name[20]; // 学生姓名
int age; // 学生年龄
int cmd; // 命令码
int stat; // 状态信息,用来回复
}info;
int main(void)
{
int sockfd = -1, ret = -1;
//这个结构体是网络编程接口中用来表示一个IP地址的,
//这个IP地址是兼容IPv4和IPv6的
struct sockaddr_in seraddr = {0};
struct sockaddr_in cliaddr = {0};
// 第1步:创建socket(AF_INET:使用IPv4进行通信,SOCK_STREAM:TCP)
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return -1;
}
printf("socketfd = %d.\n", sockfd);
// 第2步:connect链接服务器,向结构体填充服务器信息
seraddr.sin_family = AF_INET; // 设置地址族为IPv4
seraddr.sin_port = htons(SERPORT); // 设置地址的端口号信息(检测大小端并调整)
seraddr.sin_addr.s_addr = inet_addr(SERADDR); // 设置IP地址
ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret < 0)
{
perror("connect");
return -1;
}
printf("成功建立连接\n");
while (1)
{
// 回合中第1步:客户端给服务器发送信息
info st1;
printf("请输入学生姓名\n");
scanf("%s", st1.name);
printf("请输入学生年龄");
scanf("%d", &st1.age);
st1.cmd = CMD_REGISTER;
ret = send(sockfd, &st1, sizeof(info), 0);
printf("已发送%s学生的信息\n",st1.name);
// 回合中第2步:客户端接收服务器的回复
memset(&st1, 0, sizeof(st1));
ret = recv(sockfd, &st1, sizeof(st1), 0);
// 回合中第3步:客户端解析服务器的回复,再做下一步定夺
if (st1.stat == STAT_OK)
{
printf("注册学生信息成功\n");
}
else
{
printf("注册学生信息失败\n");
}
}
return 0;
}