c/c++:网络通信基础socket(网络设计模式、字节序、IP地址转换、sockaddr数据结构、套接字函数、TCP通信流程)
目录
1. 概念
1.1 网络设计模式
- B/S
- C/S
- IP和端口
- OSI/ISO 网络分层模型
2. 协议格式
3. socket编程
3.1 字节序
- 接口转换函数
3.2 IP地址转换
3.3 sockaddr数据结构
3.4 套接字函数
4. TCP通信流程
tcp 服务器server通信操作流程:
tcp 客户端client通信操作流程:
1. 概念
1.1 网络设计模式
- B/S
- 客户端: 浏览器
- 服务器: 服务器
优势: 跨平台, 开发成本低
劣势:
是的协议的固定的: http, https
不能处理大的数据
- C/S
- 客户端: 桌面应用程序
- 服务器: 后台服务器
优势: 可以处理大量的磁盘数据
劣势: 如果跨平台, 需要重新开发, 成本高
- IP和端口
- IP地址
- IPV4
- 实际是一个32位的整形数 -> 本质 -> 4字节 int a;
- 我们看的的不是这个整形数, 点分十进制字符串 -> 192.168.247.135
- 分成了4份, 每份1字节, 8bit -> char , 最大值为 255 -> 最大取值: 255.255.255.255
- IP可以有多少个 2^32^ - 1 个
- IPV6
- 实际是一个128位的整形数
- xxx:xxx:xxx:xxx:xxx:xxx:xxx:xxx , 分成了8分, 每份16位 -> 每一部分以16进制的方式表示
- IP可以有多少个 2^128^ - 1 个
- IP地址的作用:
- 通过IP地址能够找到某一台主机
- 端口
- 在一个主机上运行着很多进程
- 将数据发送到某台主机上的某个进程
- 如果要进程网络通信, 可以让这个进程绑定一个端口
- 通过这个端口就可以确定某个进程
- 端口号: unsigned short int -> 16位
- 端口取值范围: 0 -65535 (2^16^)
- OSI/ISO 网络分层模型
> OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织组织)在1985年研究的网络互联模型。
- 七层模型
底层 --------->上层
物 数 网 传 会 表 应
> - 物理层:
> - 物理层负责最后将信息编码成电流脉冲或其它信号用于网上传输
> - 数据链路层:
> - 数据链路层通过物理网络链路供数据传输。
> - 规定了0和1的分包形式,确定了网络数据包的形式;
> - 网络层
> - 网络层负责在源和终点之间建立连接;
> - 此处需要确定计算机的位置,怎么确定?IPv4,IPv6
> - 传输层
> - 传输层向高层提供可靠的端到端的网络数据流服务。
> - 每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信
> - 会话层
> - 会话层建立、管理和终止表示层与实体之间的通信会话;
> - 建立一个连接(自动的手机信息、自动的网络寻址);
> - 表示层:
> - 对应用层数据编码和转化, 确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;
> - 可以理解为:解决不同系统之间的通信,eg:手机上的QQ和Windows上的QQ可以通信;
> - 应用层:
> - 规定数据的传输协议
四层模型
2. 协议格式
3. socket编程
// 套接字通信分两部分:
- 服务器端: 被动接受连接的角色, 不会主动发起连接
- 客户端通信: 主动向服务器发起连接
socket是一套通信接口, 下linux和windows都可以使用, 但是有细微差别
3.1 字节序
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)
- 概念
- Little-Endian -> 主机字节序
- 有一个数据: 0x12345678, 在内存中进行存储
- 内存的低地址位存储数据低位字节, 内存高地址位存储数据的高位字节
- Big-Endian -> 网络字节序
- 内存的低地址位存储数据高位字节, 内存的高地址位存储数据的低位字节
- 字节序举例
// 使用16进制在内存中表示这两个数,即:
- 0x12 34 56 78 -> 四字节 char -> 255 -> ff
- 0x11223344 -> 四字节
- 小端
低地址位 -------------> 高地址位
0x78 0x56 0x34 0x12
0x44 0x33 0x22 0x11
- 大端
低地址位 -------------> 高地址位
0x12 0x34 0x56 0x78
0x11 0x22 0x33 0x44
- 接口转换函数
BSD Socket提供了封装好的转换接口,方便程序员使用。
从主机字节序(h)到网络字节序(n)的转换函数:htons、htonl;
从网络字节序(n)到主机字节序(h)的转换函数:ntohs、ntohl。
#include <arpa/inet.h>
// shot int -> 4字节(64位)
// h -> host
// n -> network
// s -> short
// l -> long
// xtoxs() -> 进行端口转换
uint16_t htons(uint16_t hostshort);
参数: 主机字节数的short型数值 -> 要转换的数(主机)
返回值: 转换之后得到是数据 (网络字节序)
uint16_t ntohs(uint16_t netshort);
// long -> 8字节(64位)
// xtoxl() -> 进行IP转换
uint32_t htonl(uint32_t hostlong);
uint32_t ntohl(uint32_t netlong);
3.2 IP地址转换
#include <arpa/inet.h>
// 字符串: 192.168.1.100 (点分十进制字符串)
// p -> 点分十进制字符串 IP
// n -> network
// 将主机字节序的 字符串IP -> 网络字节序的 整形数
int inet_pton(int af, const char *src, void *dst);
参数:
- af: 地址族协议, ipv4, ipv6
ipv4: AF_INET, ipv6:AF_INET6
- src: 点分十进制字符串 IP
- dst: 传出参数, 执行一块内存的地址, 将转换得到的网络字节序的整形数存储到这块内存中
返回值:
-1: 失败
1: 成功
0: 查字典
// 网络字节序的整形IP -> 点分十进制字符串 IP
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
- af: 地址族协议, ipv4, ipv6
ipv4: AF_INET, ipv6:AF_INET6
- src: 指向要转换的 网络字节序的整形IP 地址
- dst: 转换成功之后的 点分十进制字符串 存储的位置
- size: 修饰的就是第三个参数 dst 对应的内存大小
返回值:
NULL: 失败
非空指针, 指向第三个三种指针的内存: 成功
3.3 sockaddr数据结构
结构体 sockaddr、sockaddr_in用于网络通信
结构体 sockaddr_un用于进程间通信
结构体 sockaddr_in用于ipv6通信
由于结构体sockaddr需要用指针偏移添加IP地址,这样很麻烦,在实际中我们使用sockaddr_in来添加端口号、IP地址。再强转成sockaddr类型,因为这2个结构体大小一样,后面的服务器—客户端程序会有具体体现。
struct sockaddr {
sa_family_t sa_family; // 地址族协议, ipv4, ipv6
char sa_data[14];
}
struct sockaddr_in
{
sa_family_t sin_family; IP选择AF_INET(ipv4)、AF_INET6(ipv6)
in_port_t sin_port; 端口(网络字节序:htons() )
struct in_addr sin_addr; IP地址(网络字节序:inet_pton() )
//预留空间:
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr; IP地址(网络字节序:inet_pton() )
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
3.4 套接字函数
#include <arpa/inet.h>
// 创建一个套接字
int socket(int domain, int type, int protocol);
参数:
- domain: 地址族协议
AF_INET: ipv4
AF_INET6: ipv6
AF_UNIX, AF_LOCAL: 进行本地套接字通信(进程间通信)
- type: 通信过程中使用的协议
SOCK_STREAM: 流式协议
SOCK_DGRAM: 报式协议
- protocol: 一般写0
- SOCK_STREAM: 流式协议默认使用使用: tcp
- SOCK_DGRAM: 报式协议默认使用使用: udp
返回值: 这个文件描述符操作的是内核缓冲区
成功: 文件描述符 > 0
失败: -1
// 绑定函数 -> 将fd 和本地的 IP + Port进程绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd: 通过socket函数得到的
- addr: 需要将IP和Port初始化到这个结构体中
- addrlen: 第二个参数结构体占的内存大小
// 设置监听
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
参数:
- sockfd: 通过socket函数得到的
- backlog: 已经连接成功, 但是还没有被处理的连接指定的数值不能大于/proc/sys/net/core/somaxconn 中存储的数据, 默认为128
// 默认是一个阻塞函数, 阻塞等待客户端请求。请求到达, 接收客户端连接,得到一个用于通信的文件描述符
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
- sockfd: 用于监听的文件描述符(套接字)
- addr: 传出参数, 记录了连接成功的客户端的IP和端口信息
- addrlen: 第二个参数结构体对应的内存大小
返回值:
- 成功: 通信的文件描述符 > 0
- 失败: -1
// 客户端使用该函数连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd: 用于通信的文件描述符
- addr: 客户端要连接的服务器的地址信息
- addrlen: 第二个参数结构体占的内存大小
返回值:
连接成功: 0
连接失败: -1
// 写数据
ssize_t write(int fd, const void *buf, size_t count);
// 读数据
ssize_t read(int fd, void *buf, size_t count);
4. TCP通信流程
// tcp / udp-> 传输层协议
tcp: 面向连接的, 安全的, 流式传输协议
- 安全: 不会丢数据
udp: 面向无连接的, 不安全, 报式传输协议
tcp 服务器通信操作流程:
1. 创建一个用于监听的套接字
- 监听: 监听有客户的连接
- 套接字: 这个套接字是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定 (IP和端口 == 服务器地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听, 监听的fd开始工作
4. 阻塞等待, 当有客户端发起连接, 解除阻塞, 接受客户端的连接, 会得到一个用户通信的套接字(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束, 断开连接
tcp 客户端的通信流程:
1. 创建一个用于通信的套接字 (fd)
2. 连接服务器, 需要指定连接的服务器的 IP 和 Port
3. 连接成功, 客户端可以直接和服务通信
- 接收数据
- 发送数据
4. 断开连接
tcp 服务器server通信操作流程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1.创建用于监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
exit(0);
}
// 2.绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET; //ipv4
addr.sin_addr.s_addr = INADDR_ANY; //获取IP的操作交给了内核
// 上面的代码等价于:inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
addr.sin_port = htons(8989); //端口
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(0);
}
// 3.设置监听
int lis_ret = listen(fd, 100);
if (lis_ret == -1)
{
perror("listen");
exit(0);
}
// 4.等待被连接
struct sockaddr_in addr_cli;
int len = sizeof(addr_cli);
int connfd = accept(fd, (struct sockaddr*)&addr_cli, &len);
if (connfd == -1)
{
perror("accept");
exit(0);
}
// 通讯
while (1)
{
// 读数据
char recvBuf[1024];
read(connfd, recvBuf, sizeof(recvBuf));
printf("recv buf : %s\n", recvBuf);
// 写数据
write(connfd, recvBuf, strlen(recvBuf));
}
//释放
close(fd);
close(connfd);
return 0;
}
tcp 客户端client通信操作流程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建用于通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
// 2. 连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET; // ipv4
addr.sin_port = htons(8989); // 服务器监听的端口, 字节序应该是网络字节序
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("connect");
exit(0);
}
int i = 0;
// 通信
while(1)
{
// 读数据
char recvBuf[1024];
// 写数据
sprintf(recvBuf, "data: %d\n", i++);
write(fd, recvBuf, strlen(recvBuf));
// 如果客户端没有发送数据, 默认阻塞
read(fd, recvBuf, sizeof(recvBuf));
printf("recv buf: %s\n", recvBuf);
sleep(1);
}
// 释放资源
close(fd);
return 0;
}