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 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Linux常用的网络编程函数 -> 正文阅读

[网络协议]Linux常用的网络编程函数

? ? ? ? 由于学习网络编程时间不长,各个网络编程函数,不是很熟悉,也记得不够精确。每次都查半天,经常烦恼于此。索性把它们都记录下来,便于自己记忆以及日后查阅、回顾。

主要介绍:socket、bind、listen、connect、accept、send、sendto、recv、recvfrom、close、shutdown。

1、socket函数

在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:

int socket(int af, int type, int protocol);

1) af 为地址族(Address Family),也就是 IP 地址类型,常用的有?AF_INET?和?AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如?1030::C9B4:FF12:48AA:1A2B。
????????需要记住127.0.0.1,它是一个特殊IP地址,表示本机地址。

? ? ? ? 注意:也可以使用 PF(Protocol Family的简写) 前缀,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。

2) type 为数据传输方式/套接字类型,常用的有?SOCK_STREAM(流格式套接字/面向连接的套接字)?和?SOCK_DGRAM(数据报套接字/无连接的套接字)。

3) protocol 表示传输协议,常用的有?IPPROTO_TCP?和?IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
4) 返回值,-1表示出错,非负表示成功,返回值为socket文件描述符。


有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?
????????一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统就没办法自动推演了。
? ? ? ? 如果使用 IPv4 地址,参数 af 的值就应设为 AF_INET,此时如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() 函数:

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议

这种套接字称为 TCP 套接字。

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP,因此可以这样来调用 socket() 函数:

int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议

这种套接字称为 UDP 套接字。

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

2、bind函数

bind() 函数的原型为:

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

下面介绍Linux下的bind函数,Windows 与此类似。

????????sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。失败返回-1,成功返回非0。

下面的代码,将创建的套接字与IP地址 127.0.0.1、端口7777 绑定:

    int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建套接字
    if (listenfd == -1)
    {
        printf("create socket failed.\n");
        return -1;
    }
    
    struct sockaddr_in serv_addr;   //创建sockaddr_in结构体变量
    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(7777); //端口7777

    if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)  //将套接字和IP、端口绑定
    {
        printf("bind socket failed.\n");
        return -1;
    }

????????这里使用 sockaddr_in 结构体,然后再强制转换为 sockaddr 类型,因为sockaddr_in 结构体赋值比较方便,大小与sockaddr相等。serv_addr.sin_addr.s_addr 也可以赋值,serv_addr.sin_addr.s_addr = htonl(INADDR_ANY),这样就能监听服务器所有IP了。

sockaddr_in 结构体

接下来不妨先看一下 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填充
};

1) sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。
2) sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。
端口号需要用 htons() 函数转换。
3) sin_addr 是 struct in_addr 结构体类型的变量,下面会详细讲解。
4) sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。

in_addr 结构体

sockaddr_in 的第3个成员是 in_addr 类型的结构体,该结构体只包含一个成员,如下所示:

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

in_addr_t 在头文件 <netinet/in.h> 中定义,等价于 unsigned long,长度为4个字节。也就是说,s_addr 是一个整数,而IP地址是一个字符串,所以需要?inet_addr() 函数进行转换,例如:

unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);

运行结果:
16777343

????????为什么要搞这么复杂,结构体中嵌套结构体,而不用 sockaddr_in 的一个成员变量来指明IP地址呢?socket() 函数的第一个参数已经指明了地址类型,为什么在 sockaddr_in 结构体中还要再说明一次呢,这不是啰嗦吗?

这些繁琐的细节确实给初学者带来了一定的障碍,我想,这或许是历史原因吧,后面的接口总要兼容前面的代码。

为什么使用?sockaddr_in 而不使用?sockaddr

bind() 第二个参数的类型为 sockaddr,而代码中却使用 sockaddr_in,然后再强制转换为 sockaddr,这是为什么呢?

sockaddr 结构体的定义如下:

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};

下图是 sockaddr 与 sockaddr_in 的对比(括号中的数字表示所占用的字节数):


????????sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
????????可以认为,sockaddr 是一种通用的结构体,用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 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
};

正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

3、connect() 函数

connect() 函数用来建立连接,它的原型为:

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

各个参数的说明和 bind() 相同,不再赘述。

4、listen() 函数

通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:

  1. int listen(int sock, int backlog); //Linux
  2. int listen(SOCKET sock, int backlog); //Windows

sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度,即最大监听数量。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。

请求队列

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

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。

如果将 backlog 的值设置为?SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到?WSAECONNREFUSED 错误。

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

5、accept() 函数

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:

  1. int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); //Linux
  2. SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen); //Windows

它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。

????????accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
????????最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-12-14 16:19:48  更:2021-12-14 16:19: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年11日历 -2024/11/26 10:18:27-

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