1、socket() 函数:创建套接字

Linux 中的一切都是文件,每个文件都有一个整数类型的文件描述符;socket 也是一个文件,也有文件描述符。使用 socket() 函数创建套接字以后,返回值就是一个 int 类型的文件描述符。

在 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 前缀,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 传输协议。
有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?
      正如大家所想,一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。
      本教程使用 IPv4 地址,参数 af 的值为 PF_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()和connect()函数:绑定套接字并建立连接

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。

2.1 bind() 函数

bind() 函数的原型为:

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

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
      下面的代码,将创建的套接字与IP地址 127.0.0.1、端口 1234 绑定:

//创建套接字
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 结构体,然后再强制转换为 sockaddr 类型,后边会讲解为什么这样做。

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_INET6in_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 使用不便,才针对不同的地址类型定义了不同的结构体。

2.2 connect() 函数

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

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

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

3、listen()和accept()函数:让套接字进入监听状态并响应客户端请求

对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。

3.1 listen() 函数

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

int listen(int sock, int backlog);

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

请求队列

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
      缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。
      如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。
      当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到 WSAECONNREFUSED 错误。
      注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。

3.2 accept() 函数

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

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

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

4、write()/read():发送数据和接收数据

Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。
      前面我们说过,两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。

write() 的原型为:

ssize_t write(int fd, const void *buf, size_t nbytes);

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。

size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 "size_t" 前面加了一个"s",代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。

write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

read() 的原型为:

ssize_t read(int fd, void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。
      read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

嵌入式Linux系统:应用开发基础_socket网络通信函数介绍相关推荐

  1. 嵌入式linux系统应用开发

    关于嵌入式系统   平时大家说的嵌入式其实范围比较广的,是一种软硬件可裁剪,以应用为中心开发的专用系统,硬件平台可以是单片机,或者以ARM系列的处理器.单片机一般直接裸奔程序,不过现在有了好多基于单片 ...

  2. 嵌入式LINUX系统程序开发

    嵌入式LINUX系统程序开发 1.Ubuntu 18.04镜像源配置 2.Ubuntu上用gcc进行编写程序 2.1.gcc的安装 2.2.gcc的常用命令 2.3.编译运行程序 3.Windows下 ...

  3. linux spi 工作模式,嵌入式linux系统的开发——SPI Bit-banging方法的实现

    前言 SPI(Serial Peripheral Interface)是一种应用广泛的通信总线,通常微处理器上会集成SPI模块以支持该通信协议,输出正确的信号的时序,并保证时序间同步,实现与外部SPI ...

  4. linux课程_【课程完结】嵌入式Linux应用/驱动开发基础知识两大篇章已全部录制完毕 共72集...

    完结撒花 <第四篇嵌入式Linux应用开发基础知识> <第五篇嵌入式Linux驱动开发基础知识> 两大篇章已全部录制完毕 共计 72 集 01 嵌入式Linux应用开发基础知识 ...

  5. 嵌入式linux 分区挂载,嵌入式linux系统的开发——文件系统的分区和挂载

    前言 当我们新买的硬盘安装在电脑上后,必须要对硬盘分区和文件系统格式化后才能使操作系统能够在分区内正常读写和保存文件.所谓分区即是将硬盘划分为多个空间范围,我们可以在不同的分区内存放不同的文件以便于管 ...

  6. 嵌入式linux界面开发,嵌入式Linux系统图形及图形用户界面

    本文首先概述了 Linux 图形领域的基本设施,然后描述了一些可供嵌入式 Linux 系统使用的高级图形库以及图形用户界面支持系统.希望对嵌入式 Linux 系统的开发有所帮助. 1 Linux 图形 ...

  7. 嵌入式linux系统中常用的文件系统

    原文:https://blog.csdn.net/li_wen01/article/details/80090624 嵌入式linux系统中常用的文件系统: 闪存主要有NOR和NAND两种技术:因为F ...

  8. 嵌入式Linux系统图形及图形用户界面综述--2005-04-20博客搬家

     原地址:http://fengxupeng.bokee.com/1260734.html 本小节首先向读者描述 Linux 图形领域中常见的基础设施.之所以称为基础设施,是因为这些系统(或者函数 ...

  9. 嵌入式Linux系统图形及图形用户界面综述

    嵌入式Linux系统图形及图形用户界面综述 作者:魏永明(ymwei@minigui.org). 声明:任何人可以以任何方式复制.修改,发布或者转贴这篇文章的部分或全部内容,前提是完整保留这一版权声明 ...

最新文章

  1. 【java 性能优化实战】4 工具实践:基准测试 JMH,精确测量方法性能
  2. 50位全球专家畅谈人工智能治理进程——结伴前行,合作共赢(附下载)
  3. linux命令安装openssl函数库,ubuntu安装openssl库
  4. php 注册回调函数,PHP回调函数
  5. 基于STM32,无人船岸基信息处理代码--python实现
  6. 双11,2分钟狂挣20亿的神秘大厂,急招.NET!
  7. 可以使用田忌赛马类贪心问题的前提(或说 如何判断题目符合田忌赛马类贪心问题)
  8. static全局变量与普通的全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别?...
  9. 《区块链开发指南》一一导读
  10. linux普通用户配置自己的python环境
  11. 阿里云容器Kubernetes监控(五) - 离线存储与归档Kubernetes事件
  12. Oracle ORA-01033: ORACLE initialization or shutdown in progress 错误解决办法Windows版(手贱强制重启电脑的后果)...
  13. 元宇宙里过节,英伟达快速打造「冬日仙境」,占地 16 万平米!
  14. 商业的10个最佳Android应用程序模板
  15. 如何在word中的图片上画圈标注_word图片上画圈 如何在word图片中进行画圈标示...
  16. ansys linux卸载干净,怎么把ansys删除干净
  17. 【示波器专题】示波器带宽对测量的影响
  18. GDUT2016年ACM新生杯初赛题解
  19. Arduino 调用Stepper库驱动28BYJ-48步进电机,电机振动不转、无法反方向转的解决办法
  20. 埃米,音译为埃,符号为Å,1Å等于10^(-10)m,即纳米的十分之一

热门文章

  1. 专访香港投资推广署夏多希:与内地金融科技合作的机会在哪里?
  2. iOS学习笔记2(结合项目)--Category和Extension的区别
  3. php做端口转发,利用PHP和linux shell在ubuntu服务器实现自动端口转发
  4. 贴片元件拖锡法焊接教程(图解)
  5. 数字下变频(DDC)_数字上变频(DUC)
  6. Photoshop CS5初学者必读(29)——通道混合器
  7. 创业3.1创业机会与创业风险
  8. 最小二乘法曲线拟合y=c1+c2x+c3x^2——java版
  9. Dynamic Model
  10. 认识你的敌人_你最大的敌人就是你自己