10_socket套接字
目录1. 概念1.1、网络设计模式1.1.1 B/S1.1.2 C/S1.2、所谓的服务器开发?1.3、IP和端口1.3.1 IP地址1.3.2 端口1.4、OSI/ISO 网络分层模型2. 协议格式3. socket编程3.1 字节序3.2 IP地址转换3.3 sockaddr数据结构3.4 套接字函数4. TCP通信流程服务器端-通信流程:客户端-通信流程:代码: https://github
目录
代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
1. 概念
1.1、网络设计模式
1.1.1 B/S
- Browser -> 浏览器 -> 客户端角色
- html、css、js
- 跨平台方便
- 只能使用http/https协议
- 缓存的数据比较少
- Server -> 服务器
1.1.2 C/S
- client -> 桌面应用程序(带窗口的)
- 通信协议可以随意选择
- 可以大量缓存或加载磁盘数据
- 开发成本高, 不同平台需要开发对应的app
- server -> 服务器
1.2、所谓的服务器开发?
- 不是写服务器
- 在已经存在的服务器上开发应用程序
1.3、IP和端口
# 只要电脑/终端能上网, 那么其必然有一个IP地址
1.linux中查看IP地址:ifconfig
inet addr:192.168.237.131
2.window中查看IP地址:ipconfig
# 在网络(局域网/外网)通信的时候通过IP地址定位一台主机
1.3.1 IP地址
- IPV4 -> 常用
- 实际是一个32位的整形数 -> 本质 -> 4字节 int a;
- 我们看到的不是这个整形数, 点分十进制字符串 -> 192.168.247.135
- 分成了4份, 每份1字节, 8bit -> char , 最大值为 255 -> 最大取值: 255.255.255.255
- IP可以有多少个 232 - 1 个
- IPV6 -> 正在推广, 还没有被广泛应用
-
实际是一个128位的整形数
-
xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx , 分成了8份, 每份2字节 -> 每一部分以16进制的方式表示
CDCD:910A:2222:5498:8475:1111:3900:2020
-
IP可以有多少个 2128 - 1 个
-
1.3.2 端口
- 只有在网络通信的时候才需要端口
- 一个IP定位一台主机, 这个电脑上运行了若干个应用程序
- 发送的数据要给到某一个运行的程序, 需要精确定位
- 通过端口可以定位到当前终端的某个进程
- 发送的数据要给到某一个运行的程序, 需要精确定位
- 在程序中如何表示一个端口:
unsigned short int
-> 16位- 取值范围: 0 - 65535
- 默认: 0 -1024 是给操作系统预留的
- 主机上安装的应用程序, 通信也需要使用端口 (1025 - 5000)
- 写程序的时候, 指定端口: 5000以上
1.4、OSI/ISO 网络分层模型
OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织组织)在1985年研究的网络互联模型。
-
四/七层模型
底层 ---------> 上层 七层模型: 1.物理层 2.数据链路层 3.网络层 4.运输层 5.会话层 6.表示层 7.应用层 四层模型: 1.网络接口层 网际层IP 运输层 应用层 物理层.数据链路层 网络层 运输层 会话层.表示层.应用层
- 物理层:
- 物理层负责最后将信息编码成电流脉冲或其它信号用于网上传输
- 数据链路层:
- 数据链路层通过物理网络链路供数据传输。
- 规定了0和1的分包形式,确定了网络数据包的形式;
- 网络层
- 网络层负责在源和终点之间建立连接;
- 此处需要确定计算机的位置,怎么确定?IPv4,IPv6
- 运输层
- 运输层向高层提供可靠的端到端的网络数据流服务。
- 每一个端口号,该层就是端口与端口的通信
- 会话层
- 会话层建立、管理和终止表示层与实体之间的通信会话;
- 建立一个连接(自动的手机信息、自动的网络寻址);
- 表示层:
- 对应用层数据编码和转化, 确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;
- 可以理解为:解决不同系统之间的通信,eg:手机上的QQ和Windows上的QQ可以通信;
- 应用层:
- 规定数据的传输协议
2. 协议格式
协议: 网络通信的时候发送的数据的格式, 这个格式是双方约定好的, 这样就能正确的对数据进行解析
- 一般我们使用的协议都是标准协议
- ftp
- ssh
- http、telnet、OSPF
- tcp、udp
- ip、ICMP
- ARP、RARP
- 协议是可以自定义的
-
协议的概念
- TCP协议 -> 传输层协议
- UDP协议 -> 传输层协议
- IP协议 -> 网络层协议
- 以太网帧协议
- 数据的封装
3. socket编程
Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。
// socket是一套网络通信接口(API)
网络通信是怎么回事?
1.网络通信分为两部分
- 客户端
- 服务器端
2.服务器端:
- 提供服务, 不会主动连接客户端, 永远是被动接受连接的角色
- 需要先于客户端启动
- 需要绑定一个固定的端口
3.客户端:
- 主动连接服务器
- 客户端需要知道服务器的IP和端口
宏观上看客户端和服务器通信 -> 主机和主机之间的通信
如果主机对应的平台不同, 数据从主机A发送到主机B, 数据在解析过程中会不会有问题?
-->有字节序的问题(数据的存储顺序)
Intel的cup是x86架构
数据的存储格式: 低位字节存储在内存的低地址位, 高位字节存储在内存的高地址位
如果不是x86架构(有一些)
低位字节存储在内存的高地址位, 高位字节存储在内存的低地址位
3.1 字节序
在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。
目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-Endian 和 Little-Endian,下面先从字节序说起。
《格列佛游记》中记载了两个征战的强国,你不会想到的是,他们打仗竟然和剥鸡蛋的姿势有关。
很多人认为,剥鸡蛋时应该打破鸡蛋较大的一端,这群人被称作“大端(Big endian)派”。可是当今皇帝的祖父小时候吃鸡蛋的时候碰巧将一个手指弄破了。所以,他的父亲(当时的皇帝)就下令剥鸡蛋必须打破鸡蛋较小的一端,违令者重罚,由此产生了“小端(Little endian)派”。
老百姓们对这项命令极其反感,由此引发了6次叛乱,其中一个皇帝送了命,另一个丢了王位。据估计,先后几次有11000人情愿受死也不肯去打破鸡蛋较小的一端!
-
概念
1、Little-Endian(小端) :主机字节序 host
- 低低, 高高
- 低位字节存储在内存的低地址位, 高位字节存储在内存的高地址位
2、Big-Endian(大端) : 网络字节序 net
- 低高, 高低
- 低位字节存储在内存的高地址位, 高位字节存储在内存的低地址位
-
字节序举例
// 单字节没有字节序的问题 // 使用16进制在内存中表示这两个数,即: // 一个字节: 8bit -> char -> 255(十进制) -> oxff(16进制) - 0x12 34 56 78 大端方式存储: 内存低地址位 ---------------- > 内存的高地址位 0x12 0x34 0x56 0x78 小端方式存储: 内存低地址位 ---------------- > 内存的高地址位 0x78 0x56 0x34 0x12
- 小端
- 大端
-
函数
BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
#include <arpa/inet.h> // h : host -> 主机字节序 // to:to // n : net -> 网络字节序 // s : short -> 16位整形数 // l : long -> 32位整形数 // 主机字节序(小端) -> 网络字节序(大端) uint16_t htons(uint16_t hostshort); // 对端口进行转换!!!!!!!!! // 网络字节序(大端) -> 主机字节序(小端) uint16_t ntohs(uint16_t netshort); // 对端口进行转换!!!!!!!!! // 主机字节序(小端) -> 网络字节序(大端) uint32_t htonl(uint32_t hostlong); // 对IP进行转换, 不常用 // 网络字节序(大端) -> 主机字节序(小端) uint32_t ntohl(uint32_t netlong); // 对IP进行转换, 不常用
3.2 IP地址转换
#include <arpa/inet.h>
// 主机字节序(小端) -> 网络字节序(大端)
// 点分十进制字符串(对应本地一个小端的整数) -> 大端的整数
int inet_pton(int af, const char *src, void *dst);
参数:
- af: 地址族协议
- AF_INET: 转换的是ipv4的ip地址
- AF_INET6: 转换的是ipv6的ip地址
- src: 字符串类型的IP地址
- dst: 转换之后得到的整数, 存储在dst指针指向的内存中 -> 传出参数
返回值:
- 成功: 0
- 失败: -1
// 网络字节序(大端) -> 主机字节序(小端)
// 大端的整数(网络字节序) -> 点分十进制字符串(对应本地一个小端的整数)
// 192.168.111.222
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
- af: 地址族协议
- AF_INET: 转换的是ipv4的ip地址
- AF_INET6: 转换的是ipv6的ip地址
- src: 传入参数, 对应是要转换的大端的整形数IP
- dst: 存储转换之后的字符串类型的IP地址, 该指针要指向一块有效内存
- size: 修饰dst指向的内存大小(17字节正好)
3.3 sockaddr数据结构
// 这个结构体存储端口和IP地址的
// 客户端 -> 服务器(IP, Port)
// 1、struct sockaddr
// 往里面写入数据比较费劲
// 所以我们使用struct sockaddr_in代替truct sockaddr
struct sockaddr
{
sa_family_t sa_family; // 地址族协议, ipv4, ipv6
char sa_data[14];
}
//2、struct sockaddr_in
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))
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议 */
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* 32位的整型IP地址 */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
//最后一个参数忽略
};
3.4 套接字函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h> // 包含这个头文件, 上边的两个就可以不写了
// socket()//创建用于监听的文件描述符/套接字
// 创建一个套接字, 目的:得到一个文件描述, 对内核内存进行操作
int socket(int domain, int type, int protocol);
参数:
- domain: 地址族协议
AF_INET: 使用的ip地址是ipv4
AF_INET6: 使用的ip地址是ipv6
- type: 指定使用的传输层协议
- SOCK_STREAM: 流式协议
- SOCK_DGRAM: 报式协议(报文)
- protocol: 默认写0
- 如果第二个参数type == SOCK_STREAM, 0代表默认流式协议就是tcp
- 如果第二个参数type == SOCK_DGRAM, 0代表默认报式协议就是udp
返回值:
成功: 文件描述符--> 用于监听 !!!
失败: -1
// int lfd = socket(AF_INET,SOCK_STREAM,0);
// bind()//将:用于监听的套接字和本地IP端口进行绑定
// 将:一个文件描述符和本地的IP端口进行绑定
struct sockaddr_in addr;
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd: 通过socket函数调用得到的文件描述符
- addr: 结构体, 其中可以存储IP和端口信息, 以及地址族协议
- IP和端口是大端类型(网络字节序), 因此初始化的时候需要转换
- addrlen: addr结构体对应的内存大小
返回值:
成功: 0
失败: -1
// listen()//设置监听(给绑定成功的文件描述符设置监听)
// 监听有没有客户端连接服务器
int listen(int sockfd, int backlog);
参数:
- sockfd: 绑定成功的文件描述符
- backlog: 同时监听的最大连接数, 最大值是128
默认值存储在// /proc/sys/net/core/somaxconn
使用cat 查看
返回值:
成功: 0
失败: -1
// accept()//等待并接受客户端请求-建立连接-得到用于通信的文件描述符
// 1.默认调用后阻塞线程
// 2.当有客户端连接的时候, 该函数解除阻塞, 和客户端建立连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
- sockfd: 用于监听的套接字
- addr: 传出参数, 里边有连接成功的客户端的IP和端口
- 得到的IP和端口是大端, 如果想使用需要转换(小端)
- addrlen: 传入传出参数
- 传入: addr结构体占用的内存大小
- 传出: addr结构体占用的内存大小
返回值:
成功: 返回一个文件描述符, 用于通信!!!
失败: -1
// connect()//客户端使用该函数连接服务器
// 阻塞函数, 连接成功之后, 解除阻塞
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd: 通信的文件描述符, 通过socket()得到
- addr: 客户端要连接的服务器的IP和端口信息
- IP和端口是大端类型(网络字节序), 因此初始化的时候需要转换
- addrlen: addr结构体占的内存大小
返回值:
成功: 0
失败: -1
// 套接字通信过程中: 读写是默认阻塞的,可以修改文件描述符的属性变为非阻塞
// write()/send()//写数据
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 默认: flag = 0
// read()/recv()//读数据
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 默认: flag = 0
4. TCP通信流程
TCP: 传输层通信协议
面向连接的, 安全的流式传输协议
- 连接过程: 需要三次握手
- 断开连接: 需要四次挥手
- 使用tcp进行数据传输, 不会丢失数据
- 有数据校验机制, 如果数据丢失了, 会进行数据重传
- 流式:
- 数据的接收和发送端对数据的操作的量可以是不对等的
服务器端-通信流程:
// 有两种文件描述符: 监听(1个), 通信(N个, 服务器和多少客户端成功建立连接, 就有多少个)
// 1.通过文件描述符进行网络IO操作, 数据存储到内核的内存缓冲区中,
// 网络通信和管道一样对应的也是内存中的一块内存
// 2.通过得到的文件描述符就可以操作内核中的内存数据了
1. 创建套接字, 得到一个文件描述符
int fd = socket();
2. 将第一步得到的文件描述符和本地的IP端口进行绑定
bind();
3. 设置监听(绑定成功的文件描述符) -> 成功之后可以接收客户端连接
listen();
4. 如果有客户端连接服务器, 服务器接受请求并建立连接, 得到一个文件描述符
int fd1 = accept();
5. 通信
- 接收数据:read(); / recv();
- 发送数据:write(); / send();
6. 通信完成, 关闭文件描述符
close();
// 01server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main()
{
//1.创建监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
exit(0);
}
//2.绑定本地IP和端口
struct sockaddr_in addr;
addr.sin_port = htons(9000); //空闲端口即可
inet_pton(AF_INET, "192.168.184.132", &addr.sin_addr.s_addr);
addr.sin_family = AF_INET;
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(0);
}
//3.设置监听
ret = listen(fd, 128);
if (ret == -1)
{
perror("listen");
exit(0);
}
//4.阻塞等待客户端连接,并且接受建立连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
printf("等待客户端连接...\n");
int cfd = accept(fd, (struct sockaddr *)&cliaddr, &len);
if (cfd == -1)
{
perror("accept");
exit(0);
}
char buf[24];
printf("client IP:%s,port:%d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, buf, sizeof(buf)),
ntohs(cliaddr.sin_port));
//5.通信
while (1)
{
//接受数据
char buff[24];
int len = read(cfd, buff, sizeof(buff));
if (len > 0)
{
//接收到了对方的数据
printf("recv buf:%s\n", buff);
}
else if (len == 0)
{
printf("client disconnect...\n");
break;
}
else
{
perror("read");
break;
}
//发送数据
char *p = "你好,客户端...\n";
write(cfd, p, strlen(p) + 1);
}
//6.关闭
close(cfd);
close(fd);
return 0;
}
客户端-通信流程:
// 文件描述符1类: 通信的
1. 创建套接字
int fd = socket();
// 客户端的端口不需要调用函数进程绑定, 自动就绑定了,
// 如果非得自己绑定也可以: bind
2. 连接服务器: 在客户端需要知道服务器绑定的IP和端口是多少
connect();
3. 通信
- 接收数据:read(); / recv();
- 发送数据:write(); / send();
4. 断开连接
close():
// 01client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.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_port = htons(9000); //空闲端口即可
inet_pton(AF_INET, "192.168.184.132", &addr.sin_addr.s_addr);
addr.sin_family = AF_INET;
int ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("connect");
exit(0);
}
//3.通信
while (1)
{
//发送数据
char *p = "你好,服务器...\n";
write(fd, p, strlen(p) + 1);
//接受数据
char buff[24];
int len = read(fd, buff, sizeof(buff));
if (len > 0)
{
//接收到了对方的数据
printf("recv buf:%s\n", buff);
sleep(1);
}
else if (len == 0)
{
printf("server disconnect...\n");
break;
}
else
{
perror("read");
break;
}
}
//6.关闭
close(fd);
return 0;
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)