IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Socket代码演示 -> 正文阅读

[网络协议]Socket代码演示

声明: 本篇博客的学习途径主要为以下网站和课堂讲解,发博客目的仅为学习使用
http://c.biancheng.net/socket/

Linux下简单socket演示

在这里插入图片描述


Linux中的Socket通讯函数

socket() 创建套接字

Linux 中,使用 socket() 函数创建套接字以后,返回值就是一个 int 类型的文件描述符。
头文件:<sys/socket.h>

//函数原型
int socket(int af, int type, int protocol);
  • af(Adress Family)协议族
    也就是IP地址的类型,常见的有AF_INETAF_INET6
    • AF 是“Address Family”的简写,INET是“Inetnet
      AF_INET 表示 IPv4 地址,例如 127.0.0.1 表示本机地址;
      AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

也可以使用 PF 前缀(Protocol Family)
PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。

  • type 为数据传输方式/套接字类型
    • SOCK_STREAM(流格式套接字/面向连接的套接字)
    • SOCK_DGRAM(数据报套接字/无连接的套接字)
  • protocol 表示传输协议
    • IPPROTO_TCP 表示 TCP 传输协议
    • IPPTOTO_UDP 表示 UDP 传输协议

af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字
//标准TCP套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议

//标准UDP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议

但是!有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。


服务器bind()绑定

服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定。

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux

sock 为 int类型的socket 文件描述符
addr 为 sockaddr 结构体变量的指针
addrlen 为 addr 变量的空间大小

//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
serv_addr.sin_family = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
serv_addr.sin_port = htons(1234);  //端口

//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

sockaddr_in结构体

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};

sin_prot 为端口号

uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,
例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,
所以我们的程序要尽量在 1024~65536 之间分配端口号。

sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0


in_addr 结构体
struct in_addr{
    in_addr_t  s_addr;  //32位的IP地址
};

in_addr_t 在头文件 <netinet/in.h> 中定义
in_addr_t 等价于 unsigned long,长度为4个字节

  • 为什么在填入ip地址的时候需要用到inet_addr()函数?
    ∵s_addr 是一个整数(无符号长整形),而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址

sockaddr结构体

sockaddr 结构体的定义如下:

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};
  • 这里我们使用 sockaddr_in 结构体,在bind绑定的时候,强制转换为 sockaddr 类型为什么这么做?
//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

在这里插入图片描述

sockaddr将IP地址和端口号合并到一起,用一个成员 sa_data 表示。
要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“
没有相关函数能直接给 sockaddr 类型的变量赋值,
所以使用 sockaddr_in 在一开始生成serv_addr,生成ipv4格式的addr,再通过sockaddr保存并绑定。

可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号
sockaddr_in 是专门用来保存 IPv4 地址的结构体
补充:ipv6地址结构体

struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6
    in_port_t sin6_port;  //(2)16位端口号
    uint32_t sin6_flowinfo;  //(4)IPv6流信息
    struct in6_addr sin6_addr;  //(4)具体的IPv6地址
    uint32_t sin6_scope_id;  //(4)接口范围ID
};

客户端conect()函数

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux

listen()函数进入被动监听状态

int listen(int sock, int backlog);  //Linux
  • sock 为需要进入监听状态的套接字
  • backlog 为请求队列的最大长度

被动监听

所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。


请求队列

请求队列(Request Queue):是一个缓冲区

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。

  • 手动设置请求队列大小,listen()函数的backlog参数,一般是10或者20

  • 由系统决定请求队列长度,将 backlog 的值设置为 SOMAXCONN,一般是几百或者更多。

  • 注意: 当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误

  • 注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。


accept() 函数阻塞程序执行,返回新的套接字

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux

**accept() 会阻塞程序执行(**后面代码不能被执行),直到有新的请求到来。

accept() 返回一个新的套接字来和客户端通信!!!,比如在server.cpp中,创建的套接字是serv_sock,accept返回的是clnt_sock

addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。


Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

一次通信过程:

两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。


write()从缓冲区向套接字写入数据

write() 的原型为:

ssize_t write(int fd, const void *buf, size_t nbytes);
  • fd 为要写入的文件的描述符
  • buf 为要写入的数据的缓冲区地址
  • nbytes 为要写入的数据的字节数。

size_t 是通过 typedef 声明的 unsigned int 类型
ssize_t 是通过 typedef 声明的signed int

write() 函数将缓冲区 buf 中的 nbytes 个字节写入文件 fd

  • write的返回值:成功则返回写入的字节数,失败则返回 -1。

read()从套接字读取并保存到缓冲区

read() 的原型为:

ssize_t read(int fd, void *buf, size_t nbytes);
  • fd 为要读取的文件的描述符
  • buf 为要接收数据的缓冲区地址
  • nbytes 为要读取的数据的字节数。

read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf

  • read()的返回值:成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

socket缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。
一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

在这里插入图片描述
这些I/O缓冲区特性可整理如下:

  • I/O缓冲区在每个TCP套接字中单独存在;
  • I/O缓冲区在创建套接字时自动生成;
  • 即使关闭套接字也会继续传送 输出缓冲区中遗留的数据;但是会丢失 输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);

阻塞模式

阻塞:就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。
阻塞存在与发送端到缓冲区,缓冲区到接收端;
发送端与接收端之间是非同步的!!!!

TCP套接字默认情况下是阻塞模式,也是最常用的。

当发送数据到缓冲区时

对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:
在这里插入图片描述

1.首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。

在这里插入图片描述

2.如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。

在这里插入图片描述

3.如果要写入的数据大于缓冲区的最大长度,那么将分批写入。

在这里插入图片描述

4.直到所有数据被写入缓冲区 write()/send() 才能返回。


当从缓冲区收数据时

当使用 read()/recv() 读取数据时:
在这里插入图片描述

1.首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。

在这里插入图片描述

2.如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
3.直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-07-15 16:36:12  更:2021-07-15 16:36:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/6 13:35:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码