网络编程
一、Internet与TCP/IP协议
20世纪50年代,美国领导的西方阵营,苏联领导的东方阵营。为争夺科技,军事力量的背景下产生。
美国成立国防部高级研究计划。1968年提出高级资源共享的计算机网络。后实现网络互连。最初的Internet形成。
也叫ARPAnet 阿帕网。其他国家相继建立本国的主干网并接入Internet.又MIC,Sprint 公司运营使普通家庭也可以使用。
1.1 OSI
OSI是Open System Interconnect的缩写,意为开放式系统互联。
其各个层次的划分遵循下列原则:
(1)同一层中的各网络节点都有相同的层次结构,具有同样的功能。
(2)同一节点内相邻层之间通过接口进行通信。
(3)七层结构中的每一层使用下一层提供的服务,并且向其上层提供服务。
(4)不同节点的同等层按照协议实现对等层之间的通信。
应用层: 产生网络流量的程序
表示层: 传输之前是否进行加密或者压缩处理
会话层: 查看会话,查木马 netstat-n
传输层: 可靠传输、流量控制、不可靠传输
网络层: 负责选择最佳路径、规划ip地址
数据链路层: 帧的开始和结束、透明传输、差错校验
物理层: 接口标准、电器标准、如何更快传输数据
1.2 TCP/IP
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。
- TCP/IP是在网络的使用中的最基本的通信协议。
- TCP/IP传输协议对互联网中各部分进行通信的标准和方法进行了规定。
- TCP/IP传输协议是保证网络数据信息及时、完整传输的两个重要的协议。
- TCP/IP传输协议是严格来说是一个四层的体系结构,应用层、传输层、网络层和数据链路层都包含其中。
各层的常见协议
第 | 层 | 常见协议 |
---|---|---|
1 | 应用层 | Telnet (远程登录)FTP (文件传输协议)HTTP (超文本传输协议)DNS (域名系统)SNMP (简单网络管理协议 基于UDP)SMTP (简单邮件传输) |
2 | 传输层 | TCP (传输控制协议) 可靠的UDP (用户数据报协议) 不可靠的 |
3 | 网络层 | IP协议 (网际协议)ICMP (互联网报文控制协议) IGMP (组管理协议) |
4 | 网络接口和物理层 | 以太网、令牌环网、FDDIARP (地址解析协议) RARP (逆地址解析协议) |
四层的作用
- 应用层:应用层是
TCP/IP协议
的第一层,是直接为应用进程提供服务的。- 对不同种类的应用程序它们会根据自己的需要来使用应用层的不同协议,邮件传输应用使用了SMTP协议、万维网应用使用了HTTP协议、远程登录服务应用使用了有TELNET协议。
- 应用层还能加密、解密、格式化数据。
- 应用层可以建立或解除与其他节点的联系,这样可以充分节省网络资源。
- 运输层:作为
TCP/IP协议
的第二层,运输层在整个TCP/IP协议
中起到了中流砥柱的作用。且在运输层中,TCP和UDP也同样起到了中流砥柱的作用 - 网络层:网络层在TCP/IP协议中的位于第三层。在
TCP/IP协议
中网络层可以进行网络连接的建立和终止以及IP地址的寻找等功能 - 网络接口层:在
TCP/IP协议
中,网络接口层位于第四层。由于网络接口层兼并了物理层和数据链路层所以,网络接口层既是传输数据的物理媒介,也可以为网络层提供一条准确无误的线路
OSI模型和TCP/IP模型的结构图
协议
在网络通信的过程中,将发出数据的主机称为源主机,接收数据的主机称为目的主机。
当源主机发出数据时,数据在源主机中从上层向下层传送。
- 源主机中的应用进程先将数据交给应用层,应用层加上必要的控制信息就成了报文流,向下传给传输层。
- 传输层将收到的数据单元加上本层的控制信息,形成报文段、数据报,再交给网际层。
- 网际层加上本层的控制信息,形成IP数据报,传给网络接口层。
- 网络接口层将网际层交下来的IP数据报组装成帧,并以比特流的形式传给网络硬件(即物理层),数据就离开源主机。
1.3 网络的术语
1.3.1 互联网的地址
互联网的地址必须有唯一的IP,IP地址为32位(IPv4)或者128位(IPv6)。 Internet中的主机要与别的机器通信必须具有一个IP地址,
理解源IP地址和目的IP地址
因特网中的每一台主机都有自己的IP地址,如果要实现A主机与B主机进行通信,A主机就必须要知道B主机的IP地址(目的IP),这样A主机才能向B主机发生数据;B主机接收到数据后,显然也需要A主机一个响应,那么B主机就必须要知道A主机的IP地址(源IP地址);当然这里只是大概的意思,等我们对套接字编程有了一定的了解,知道两个主机之间是如何通信的,再回过头来去理解这些协议的作用和更深层的理解;
每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由。
-
公有地址
公有地址(Public address)由Inter NIC(Internet Network Information Center因特网信息中心)负责。这些IP地址分配给注册并向Inter NIC提出申请的组织机构。通过它直接访问因特网。 -
私有地址
私有地址(Private address)属于非注册地址,专门为组织机构内部使用。以下列出留用的内部私有地址
这些32位的地址通常写成四个十进制的数,其中每个整数对应一个字节。这种表示方法称作 点分十进制 表示法(Dotted decimal notation)”。
类别 | 最大网络数 | IP地址范围 | 单个网段最大主机数 | 私有IP地址范围 |
---|---|---|---|---|
A | 126(2^7-2) | 1.0.0.1-127.255.255.254 | 16777214 | 10.0.0.0-10.255.255.255 |
B | 16384(2^14) | 128.0.0.1-191.255.255.254 | 65534 | 172.16.0.0-172.31.255.255 |
C | 2097152(2^21) | 192.0.0.1-223.255.255.254 | 254 | 192.168.0.0-192.168.255.255 |
有三类IP地址:
-
单播地址(目的为单个主机)。
-
广播地址(目的端为给定网络上的所有主机)。
-
多播地址(目的端为同一组内的所有主机)。
在Linux运行ifconfig, 如果网卡信息中包含UP BROADCAST(广播) RUNNING MULTICAST(组播),则支持广播和组播。
IP字符串<–>网络字节序
#include <arpa/inet.h>
inet_aton() //将strptr所指的字符串转换成32位的网络字节序二进制值
int inet_aton(const char *strptr,struct in_addr *addrptr);
inet_addr() //功能同上,返回转换后的地址。
in_addr_t inet_addr(const char *strptr);
inet_ntoa() //将32位网络字节序二进制地址转换成点分十进制的字符串。
char *inet_ntoa(stuct in_addr inaddr);
/* Internet address. */
struct in_addr
{
uint32_t s_addr; /* address in network byte order */
};
有关IP的结构体
struct sockaddr // IPV4
{
sa_family_t sa_family;
char sa_data[14];
}
// 因特网类型 使用时要强制转换
struct sockaddr_in
{
sa_family_t sin_family; // 协议族
in_port t sin_port; // 端口号
struct in_addr sin_addr; // IP地址结构体
unsigned char sin_zero[8]; // 填充 没有实际意义,只是为跟sockaddh结构在内存中对齐这样两者才能相互转换
};
1.3.2 端口
在网络技术中,端口(Port)大致有两种意思:
-
物理意义上的端口,比如,ADSL Modem、集线器、交换机、路由器用于连接其他网络设备的接口,如RJ-45端口、SC端口等等。
-
逻辑意义上的端口,一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。
理解源端口号和目的端口号
数据链路和IP中的地址,分别是MAC地址和IP地址。前者是用来识别同一链路中不同的计算机,后者是用来识别TCP/IP网络中互连的主机和路由器。在传输层中也有这样类似于地址的概念,那就是端口号。端口号是用来==识别同一台计算机中进行通信的不同应用程序==。因此,它也被称为程序地址。
服务器一般都是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23。任何TCP/IP实现所提供的服务都用知名的1~1023之间的端口号。这些知名端口号由Internet号分配机构( Internet Assigned Numbers Authority, IANA)来管理。
到1992年为止,知名端口号介于1~255之间。256~1023之间的端口号通常都是由Unix系统占用,以提供一些特定的Unix服务——也就是说,提供一些只有Unix系统才有的、而其他操作系统可能不提供的服务。现在IANA管理1~1023之间所有的端口号。
**大多数TCP/IP实现给临时端口分配1024~5000之间的端口号。**大于5000的端口号是为其他服务器预留的( Internet上并不常用的服务)。
1024~65535是动态端口。这些端口号一般不固定分配给某个服务,也就是说许多服务都可以使用这些端口。当程序关闭,也会同时释放这些端口供其他程序使用。
大多数Unix系统的文件/etc/services都包含了人们熟知的端口号。为了找到Telnet服务器和域名系统的端口号,可以运行以下语句: preg telnet /etc/services
进程ID与端口号的理解:
我们都知道所有进程都需要一个PID来进行表示,但是不是所有的进程都是网络进程,所以不是所有进程都需要端口号。同时一个进程可以绑定多个端口号(就像学生在学校可以有学号,在健身房可以有会员号),但是一个端口号不能被多个进程绑定。
为什么客户端一般不绑定一个端口呢?
因为一个端口只能被绑定一次。这些端口当然要优先让级服务端,由于服务端的端口不能变更,需要被其他的客户端所知。而客户端的端口不需要被其他多端知晓。
为什么tcp/ip中的端口范围为65535?
在TCP/IP协议的开头,会分别有16位来存储源端口号和目标端口号,这16位所支持的数值范围即为端口范围。由于2^16-1=65535,所以端口范围为0-65535。 在新的IPV6和ipv4上的端口限制都为65535。
端口号 | 用途 |
---|---|
0-255 | 知名端口号 |
256~1023 | Unix系统占用 |
1024~5000 | 临时端口 |
1024~49151 | 用户端口 |
5000~65535 | 服务器预留 |
重要的端口
译 | 协议 | TCP端口号 |
---|---|---|
用于万维网(WWW)服务的超文本传输协议 | HTTP | 80 |
文件传输协议 | FTP | 20(数据传输)/21(发送控制命令) |
简单邮件传输协议 | SMTP | 25 |
邮局协议版本3 | POP3 | 110 |
域名服务 | DNS | 53 |
Telnet 服务 | TELNET | 23 |
1.3.3 字节序
- Big-endian: 高位字节存入低地址,低位字节存入高地址
- Little-endian:低位字节存入低地址,高位字节存入高地址
将12345678h
写入1000h
开始的内存中,以大端序和小端序模式存放结果如下:
一般来说,x86系列CPU都是Little-endian
字节序,PowerPC通常是Big-endian
字节序。
IP是TCP/IP协议族中最为核心的协议。所有的TCP、UDP、ICMP及IGMP数据都以IP数据报格式传输。许多刚开始接触TCP/IP的人对IP提供不可靠、无连接的数据报传送服务感到很奇怪,特别是那些具有X.25或SNA背景知识的人。
不可靠( unreliable)的意思是它不能保证IP数据报能成功地到达目的地。 IP仅提供最好的传输服务。如果发生某种错误时,如某个路由器暂时用完了缓冲区,IP有一个简单的错误处理算法:丢弃该数据报,然后发送ICMP消息报给信源端。任何要求的可靠性必须由上层来提供(如TCP)。
无连接( connectionless)这个术语的意思是IP并不维护任何关于后续数据报的状态信息。 每个数据报的处理是相互独立的。这也说明,IP数据报可以不按发送顺序接收。如果一信源向相同的信宿发送两个连续的数据报(先是A,然后是B),每个数据报都是独立地进行路由选择,可能选择不同的路线,因此B可能在A到达之前先到达。
4个字节的32 bit值以下面的次序传输:首先是(0~7 bit,其次8~15 bit,然后16~23 bit,最后是24~31 bit。这种传输次序称作大端(big endian)字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。以其他形式存储二进制整数的机器,如小端(little endian)格式,则必须在传输数据之前把首部转换成网络字节序。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址存高字节,高地址存低字节。
与同一台计算机上的进程进行通信时,一般不考虑字节序。 字节序是一个处理器架构特性,用于指示像整数这样的大数据类型内部的字节如何排序。但如果涉及网络通信,那就必须考虑大小端的问题,否则对端主机识别出来的数据可能与发送端想要发送的数据是不一致的。
TCP/IP协议栈使用大端字节序。应用程序交换格式化数据时,字节序问题就会出现。对TCP/IP,地址用网络字节序来表示,所以应用程序有时候需要在处理器的字节序与网络字节序之间进行转换。以确保数据的一致性。
例如上一节的UDP段格式,地址0~1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。
但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。
同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //返回值:以网络字节序表示的32位整数
uint16_t htons(uint16_t hostshort); //返回值:以网络字节序表示的16位整数
uint32_t ntohl(uint32_t netlong); //返回值:以主机字节序表示的32位整数
uint16_t ntohs(uint16_t netshort); //返回值:以主机字节序表示的16位整数
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
1.3.4 套接字
Socket在Linux下,用于表示进程间网络通信的特殊文件类型。
本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致 。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。 欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
Socket(套接字)主要用到的协议
1、TCP(面向连接) 双方必须连接成功才能发数据,类似打电话,主要用于对传输数据精确的情况,如传输指令。
2、UDP(面向报文) 双方无须连接成功就能发数据,类似发短信,主要用于传输大数据量的情况,如视频。
总的来说:
- IP地址最大的意义在于指导一个报文该如何进行路径选择,到哪里去就是去找目标IP地址。
- 端口号的意义在于唯一的标识一台机器上的唯一一个进程。
- IP地址 + 端口号 = 能够标识互联网中的唯一一个进程!(也就是我们接下来讲的套接字);
- IP地址 + port(端口号) = socket(套接字)
1.4 网络应用编程模型
1.4.1 C/S模式
C/S(Client/Server)也叫C/S模式,C/S架构或C/S模型。
- 胖客户端编程架构
- 主要工作在客户端进行
C/S将网络事务处理分为两个部分
- 客户端:客户端(Client,也叫客户机)用于为用户提供操作,同时向网络提供请求服务的接口;
- 服务端: 服务端(Server)负责接收并处理客户端发出的服务请求,并将服务处理结果返回给客户端。
C/S即适用于实际的应用程序,又适用于真正的计算机部署。
从程序实现的角度来说,客户端和服务端实际是计算机上的两个进程的交互。服务端进程逐一等待并处理客户端请求。
运行服务端进程的计算机系统一般通过所提供的服务来命名。 例如,提供邮件服务的主机称为邮件服务器,提供远程文件访问的计算机称为文件服务器等。
应用:LOL等大型3D游戏(缓存数据图像,环境包等)
-
优点:
- 协议选用灵活。(可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯公司所采用的通信协议,即为ftp协议的修改剪裁版。)
- 将数据缓存至客户端,提高数据传输效率。
-
缺点:
- 对用户的安全构成威胁 (需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。)
- 开发工作量较大,调试困难(服务器和客户端都需要团队开发)。
1.4.2 B/S模式
B/S (Browse/Server),也叫B/S模式,B/S模型。Browse是指Web浏览器,仅使用HTTP进行通信。
极少数事务在前端实现,但主要事务逻辑在服务器端实现。
-
优点
- S架构客户端无需安装,有Web浏览器即可。
- B/S架构可以直接放在广域网上,交互性较强。
- B/S架构无需升级多个客户端,升级服务器即可。
-
缺点
- 在跨浏览器上,BS架构不尽如人意。
- 表现要达到C/S程序的程度需要花费不少精力。
- 在速度和安全性上需要花费巨大的设计成本。
B/S编程模型采用三层架构设计
- 用户界面
- 逻辑设计
- 数据支持
B/S 和 C/S 的区别
- 构建方式
一个是浏览器/服务器端架构,另一个是客户端/服务器架构
B为浏览器,浏览器即客户端,C/S需单独设计客户端 - 更新维护方式
B/S结构维护升级比较简单,而C/S结构维护升级相对困难; - 安全控制能力
C/S结构比B/S结构更安全,因为用户群相对固定,对信息的保护更强; - 用户受众
C/S用户群相对固定,而B/S相对来说很广,B/S是建立在广域网上的,适应范围强。
1.4.3 C/S工作过程
服务器端和客户端的工作过程
- 服务器首先启动监听程序,对指定的端口进行监听,等待接收客户端的连接请求。
- 客户端启动程序,请求连接服务器的指定端口。
- 服务器收到客户端的连接请求后,与客户端建立套接字连接。
- 连接建立成功,客户端与服务器分别打开两个流,其中客户端的输入流连接到服务器的输出流,服务器的输入流连接到客户端的输出流,两边的流连接成功后进行双向通信。
- 当通信完毕后,客户端和服务器两边各自断开连接。
建立连接和断开连接的细节
三次握手
最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。
- 第一次握手:客户端主动打开,发送连接请求报文段,将SYN标识位置为1。
- 第二次握手:服务器收到SYN报文段进行确认(ACK),将SYN标识位置为1,ACK置为1,这个状态被称为半连接状态。
- 第三次握手:客户端再进行一次确认(ACK),将ACK置为1(此时不用SYN)。
为什么TCP客户端最后还要发送一次确认呢?
一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
三次握手的目的
第一次:让服务器知道,从客户端到服务器的连接是通的。(客户端对服务器说:我想给你发数据)
第二次:让客户端知道,从服务器到客户端的连接是通的。(服务器对客户端说:我能收到,你发吧 — 确保上行通道)
第三次:让服务器知道,从服务器到客户端的连接是通的。(客户端对服务器说:我知道你收到我的请求了,那我发了 — 确保下行通道)
四次挥手
数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。
四次挥手的目的
因为对于断开链接,发送方和接收方都需要获取对方的终止信号(FIN)和对终止的确认信号(ACK),所以需要四次挥手。
第一次:客户端对服务器说:到点了,我要走啦。
第二次:服务器对客户端说:好吧,我知道你要走了。
第三次:服务器对客户端说:那我也走啦。
第四次:客户端对服务器说:好,我也知道你要走了。
为什么客户端最后还要等待2MSL?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
为什么建立连接是三次握手,关闭连接确是四次挥手呢?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
1.4.4 C/S编程流程
服务器端的编程流程
step | 步骤 | 需要的函数 | TCP是否必选 | UDP是否必选 |
---|---|---|---|---|
1 | 创建Socket套接字 | socket(); | ||
2 | 设置Socket属性 | setsockopt(); | 非必选 | |
3 | 绑定端口 | bind(); | ||
4 | 开启监听 | listen(); | 不需要 | |
5 | 接受客户端发送过来 的连接请求 | accept(); | 不需要 | |
6 | 交换数据 | read() /write() 、send() /recv() 、sendto() /recvfrom() | sendto() recvfrom() | |
7 | 处理事件 | 自定义函数 | ||
8 | 关闭套接字 | close() |
客户端的编程流程
step | 步骤 | 需要的函数 | TCP是否必选 | UDP是否必选 |
---|---|---|---|---|
1 | 创建套接字 | socket(); | ||
2 | 设置Socket属性 | setsockopt(); | 非必选 | |
3 | 设置端口和ip | bind(); | 非必选 | |
4 | 连接服务器 | connect() | 不需要 | |
5 | 交换数据 | read() /write() 、send() /recv() 、sendto() /recvfrom() | sendto() recvfrom() | |
6 | 处理事件 | 自定义函数 | ||
7 | 关闭套接字 | close() |
二、服务端/客服端
2.1 TCP
TCP是 Transmission Control Protocol 的缩写,即传输控制协议。
TCP与UDP的区别相当大。它充分地实现了数据传输时的各种控制功能,可以进行丢包时重发控制,还可以对次序乱掉的分包进行顺序控制。而这些再UDP中都没有。此外,TCP作为一种面向有连接的协议,只要在确认通信对端存在时才会发生数据,从而可以控制通信流量的浪费。
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
以上或许你有很多不太了解,这里你只需要知道 UDP是无连接,TCP是有连接 的即可。阅读下文套接字编程,就可以感受的出来。
2.1.1 服务端
2.1.1.1 创建套接字 socket()
用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
{
@msg
domain :指明所使用的协议族
type :指定socket类型
protocol :通常赋值为 0 ,0表示选择type类型对应得默认协议
@return
int :成功时返回文件描述符,失败返回 -1,并设置errno
}
eg:
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
domain(第一个参数) | ||
---|---|---|
AF_INET | IPv4因特网域 | |
AF_INET6 | IPv6因特网域 | |
AF_UNIX | Unix 域 | |
AF_ROUTE | 路由套接字 | |
AF_KEYE | 钥套接字 | |
AF_UNSPEC | 未拖定 |
type(第二个参数) | 描述 |
---|---|
SOCK_STREAM | TCP: 流式套接字提供可靠的、面向连接的通信流:它使用TCP协议,从而保证了数据传输的正确性和顺序性 |
SOCK_DGRAM | UDP: 数据报套接字定义了一种无连按的服,数据通过相互独立的报文进行传输, 是无序的,并且不保证是可靠,无差错的。它使用数据报协议UDP。 |
SOCK_RAW | 允许程序使用低层协议,原始科接字允许对底层协议如IP或ICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。 |
protocol(第三个参数) | 描述 |
---|---|
IPPROTO_TCP | TCP传输协议 |
IPPTOTO_UDP | UDP 传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
IPPROTO_TIPC | TIPC传输协议 |
2.1.1.2 设置scoket setscokopt()
用于任意类型、任意状态套接口的设置选项值。
setsockopt();
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
{
@msg
sockfd :套接字描述符
level :被设置的选项的级别
optname :指定准备设置的选项
*optval :指向存放选项待设置的新值的缓冲区。
optlen :optval缓冲区长度。
@return
int :成功返回0,失败返回-1,并设置errno
}
eg:
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
optname选项 SOL_SOCKET | 意义 | 类型 |
---|---|---|
SO_BROADCAST | 允许发送广播数据 | int |
SO_DEBUG | 允许调试 | int |
SO_DONTROUTE | 不查找路由 | int |
SO_ERROR | 获得套接字错误 | int |
SO_KEEPALIVE | 保持连接 | int |
SO_LINGER | 延迟关闭连接 | struct linger |
SO_OOBINLINE | 带外数据放入正常数据流 | int |
SO_RCVBUF | 接收缓冲区大小 | int |
SO_SNDBUF | 发送缓冲区大小 | int |
SO_RCVLOWAT | 接收缓冲区下限 | int |
SO_SNDLOWAT | 发送缓冲区下限 | int |
SO_RCVTIMEO | 接收超时 | struct linger |
SO_SNDTIMEO | 发送超时 | struct linger |
SO_REUSEADDR | 允许重用本地地址和端口 | int |
SO_TYPE | 获得套接字类型 | int |
SO_BSDCOMPAT | 与BSD系统兼容 | int |
SOL_SOCKET | ||
---|---|---|
IP_HDRINCL | 在数据包中包含IP首部 | int |
IP_OPTINOS | IP首部选项 | int |
IP_TOS | 服务类型 | |
IP_TTL | 生存时间 | int |
IPPRO_TCP | ||
---|---|---|
TCP_MAXSEG | TCP最大数据段的大小 | int |
TCP_NODELAY | 不使用Nagle算法 | int |
2.1.1.3 绑定IP、端口 bind()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
{
@msg
sockfd : 一个socket描述符,由socket()调用返回
*addr :是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构,这个地址结构根据地址创建,socket时的地址协议族的不同而不同。
addrlen :第二个参数中结构体的大小,一般用sizeof计算
@return
int :成功返回0,失败返回-1,并设置errno
}
eg:
// 绑定ip和端口等信息
int ret = bind(listenfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
struct sockaddr // IPV4
{
sa_family_t sa_family;
char sa_data[14];
}
// 因特网类型 使用时要强制转换
struct sockaddr_in
{
sa_family_t sin_family; // 协议族
in_port t sin_port; // 端口号
struct in_addr sin_addr; // IP地址结构体
unsigned char sin_zero[8]; // 填充 没有实际意义,只是为跟sockaddh结构在内存中对齐这样两者才能相互转换
};
2.1.1.4 建立监听listen()
listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
{
@msg
sockfd :由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,用户进程通过系统调用listen来完成这件事。
backlog :是侦听队列的长度,它的作用在于处理可能同时出现的几个连接请求。
//DoS(拒绝服务)攻击即利用了这个原理,非法的连接占用了全部的连接数,造成正常的连接请求被拒绝。
@return
int :成功返回0,失败返回-1,并设置errno
}
ret = listen(listenfd, 8);
2.1.1.5 连接 accept
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
{
@msg
sockfd : 监听套接字
*addr : 对方地址
*addrlen:地址长度
//如果不关心对方的ip和端口等 addr 和 addrlen写成NULL
//如果要关心:再用一个 Internet协议地址结构; struct sockaddr_in 类型变量保存,然后再使用
@return
int : 成功时返回连接套接字,失败时返回-1,并设置errno
}
2.1.1.5 交换数据
2.1.1.6 处理事件
2.1.1.7 关闭套接字
close(socketfd);
2.1.2 客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define SIZE 1024
int main(int argc, char **argv)
{
#if 0
if(3 != argc)
{
printf("Usage: <%s> <IP> <PORT>\n", argv[0]);
return -1;
}
#endif
//创建通信套结字 :注意:客户端 没有监听,创建的套结字直接就是通信套结字
char buf[SIZE] = {0};
int connfd = socket(PF_INET, SOCK_STREAM, 0);
if (-1 == connfd)
{
perror("socket");
return -1;
}
//以下填充的是 要连接的服务器ip 和端口等信息
#if 1
struct sockaddr_in serveraddr = {
.sin_family = PF_INET,
.sin_addr.s_addr = inet_addr("127.0.0.1"),
.sin_port = htons(6666)};
#else
struct sockaddr_in serveraddr = {0};
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
#endif
//连接服务器
int len = sizeof(serveraddr);
if (-1 == connect(connfd, (struct sockaddr *)&serveraddr, len)) //主动连接服务器
{
perror("connect");
return -1;
}
//通信
while (1)
{
fgets(buf, sizeof(buf), stdin);
write(connfd, buf, strlen(buf));
// sleep(1);
read(connfd, buf, sizeof(buf));
printf("%s\n", buf);
// write(STDOUT_FILENO, buf,strlen(buf)); //等同于 printf 输出到屏幕
}
//关闭套结字
close(connfd);
}
2.2.3 并发服务器
多个客户端需要处理,请求可能同时到来可运用循环服务器和并发服务器。
-
循环服务器:一次处理每个客户端,知到当前客户端的所有请求处理完成,再处理下一个客户端。
-
并发服务器:思想:多任务处理机制(多线程或多进程)
分别给每个客户端创建一个任务来处理,极大提高了服务器的并发处理能力。
2.2.3.1 多进程并发服务器
父进程创建连接、子进程负责与客户端的数据交互
每来一个客户端建立连接之后,会常见子进程来负责数据的交互
子进程资源释放:使用信号进行异步处理
子进程退出时会给父进程发送SIGCHLD
捕捉SIGCHLD信号,进行处理需要实现信号处理函数
不足:每个进程都需要单独分配内存空间,客户端越多,则进程越多,就会消耗更多的资源,进程之间的协同需要进程建通讯
文件描述符浪费情况:在fork之后子进程不需要监听套接字,父进程不需要客户端套接字,将不需要的文件描述符要关闭掉
/**************************************************************
* File Name : pthr_TCP_server.c
* Creat Time : 2022年11月15日 星期二 14时44分53秒
* 备注 :
***************************************************************/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <time.h>
#include <errno.h>
int conn(int conn, struct sockaddr_in *user);
void signaled(int sign);
int server_init(char *p, int port, int backlog);
int main(int argc, char *argv[])
{
int conn_fd = -1;
struct sockaddr_in caddr;
socklen_t clen = sizeof(caddr);
signal(SIGCHLD, signaled);
int listen_fd = server_init(argv[1], 6666, 15);
while(1)
{
conn_fd = accept(listen_fd, (struct sockaddr *)&caddr, &clen);
if(conn_fd == -1)
{
if(errno == EINTR)
continue;
else
{
perror("accept");
exit(0);
}
}
char *p = inet_ntoa(caddr.sin_addr);
int port = ntohs(caddr.sin_port);
printf("\e[1;32m客户端(%s:%d)已连接!\e[0m\n", p, port);
pid_t pid = fork();
if(!pid)
{
close(listen_fd);
conn(conn_fd, &caddr);
}
else if(pid > 0)
{
close(conn_fd);
continue;
}
}
return 0;
}
int server_init(char *p, int port, int backlog)
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1)
{
perror("socket");
exit(-1);
}
printf("创建成功!\n");
int a = 1;
if(-1 == setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &a,sizeof(a)))
{
perror("setsockopt");
exit(-1);
}
struct sockaddr_in listenaddr =
{
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr= inet_addr(p)
};
if(-1 == bind(listenfd, (struct sockaddr *)&listenaddr, sizeof(listenaddr)))
{
perror("bind");
exit(-2);
}
printf("绑定成功!\n");
if(-1 == listen(listenfd, backlog))
{
perror("listen");
exit(-3);
}
printf("等待客户端连接。。。。\n");
return listenfd;
}
void signaled(int sign)
{
if(sign == SIGCHLD)
{
printf("清理僵尸进程...\n");
while(waitpid(-1,NULL,WNOHANG) > 0);
}
}
int conn(int conn, struct sockaddr_in *user)
{
time_t now;
struct tm *date;
char *p = inet_ntoa(user->sin_addr);
int port = ntohs(user->sin_port);
while(1)
{
char buf[BUFSIZ] = {0};
time(&now);
date = localtime(&now);
int ret = read(conn, buf, BUFSIZ);
if(ret == 0)
{
printf("\e[31m客户端(%s:%d) \e[36m%02d:%02d:%02d \e[31m已断开\e[0m\n", p, port, date->tm_hour, date->tm_min, date->tm_sec);
exit(0);
}
printf("\e[32m%s:%d \e[36m%02d:%02d:%02d \e[1;33m<--\e[0m%s",p,port, date->tm_hour, date->tm_min, date->tm_sec,buf);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-reEH0h49-1669028155413)(05_网络编程.assets/image-20221115175610071.png)]
2.2.3.2 多线程并发服务器
#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#define SERV_ip "0"
#define SERV_PORT 6666
void *do_client(void *connfd);
int server_init(char *ip, int port, int num)
{
int listenfd, ret;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listenfd)
{
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr(ip);
ret = bind(listenfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == ret)
{
perror("bind");
exit(-1);
// goto ERROR_STEM;
}
listen(listenfd, num);
return listenfd;
#if 0
ERROR_STEM:
close(listenfd);
exit(-1);
#endif
}
//多进程服务器
int main(int argc, const char *argv[])
{
int listenfd, connfd, ret;
listenfd = server_init(SERV_ip, SERV_PORT, 8);
if (-1 == listenfd)
{
perror("server_init ");
exit(-1);
}
while (1)
{
printf("wait for connect......\n");
connfd = accept(listenfd, NULL, NULL);
printf("connnect a client!\n");
//每连接一个客户,开线程:主线程负责监听; 子线程 负责通信
pthread_t tid;
pthread_create(&tid, NULL, do_client, (void *)connfd); //创建子线程
pthread_detach(tid); //线程资源回收
}
close(listenfd);
return 0;
}
void *do_client(void *arg)
{
int connfd = (int)arg;
int ret;
char buf[1024] = {0};
while (1)
{
// memset(buf, 0, sizeof(buf));
bzero(buf, sizeof(buf));
ret = read(connfd, buf, sizeof(buf));
if (-1 == ret)
{
perror("read");
close(connfd);
exit(0);
}
else if (0 == ret)
{
printf("client quit\n");
close(connfd);
exit(0);
}
printf("recv:%s\n", buf); //
}
}
2.2.3.3 IO多路复用并发服务器
5种IO模型分别是阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型、异步IO模型;前4种为同步IO操作,只有异步IO模型是异步IO操作。
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
int server_init(char *ipaddr, unsigned short port, int backlog) //初始化服务器
{
/********************************************
AF_INET IPv4 Internet protocols
SOCK_STREAM string socket
0
*********************************************/
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return -1;
}
printf("sockfd=%d\n", sockfd);
/*************************************************************
Internet协议地址结构“ /usr/include/netinet/in.h”
struct sockaddr_in
{
u_short sin_family; // 地址族, AF_INET,2 bytes
u_short sin_port; // 端口,2 bytes
struct in_addr sin_addr; // IPV4地址,4 bytes
char sin_zero[8]; // 8 bytes unused,作为填充
};
struct in_addr{
in_addr_t s_addr; // u32 network address
};
****************************************************************/
struct sockaddr_in saddr; //定义Internet地址结构变量,保存服务器的ip及port
memset(&saddr, 0, sizeof(saddr)); // bzero
saddr.sin_family = AF_INET; //指定网络协议 IPV4
saddr.sin_port = htons(port); //指定服务器的端口号 >=5001,由主机序转网络字节序
// INADDR_ANY:任意ip地址
saddr.sin_addr.s_addr = (NULL == ipaddr) ? (htonl(INADDR_ANY)) : inet_addr(ipaddr); //指定服务器的ip地址,ip地址由点分式转32为无符号网络字节序
socklen_t slen = sizeof(saddr);
//将服务器的ip和port与sockfd绑定
int ret = bind(sockfd, (struct sockaddr *)&saddr, slen);
if (-1 == ret)
{
perror("bind");
goto ERR_STEMP;
}
printf("bind success\n");
ret = listen(sockfd, backlog); // sockfd变为监听套接字
if (-1 == ret)
{
perror("listen");
goto ERR_STEMP;
}
return sockfd;
ERR_STEMP:
close(sockfd);
return -1;
}
int main()
{
int ret;
int sockfd = server_init(NULL, 8000, 1024); //初始化服务器
if (-1 == sockfd)
{
printf("server_init error\n");
return -1;
}
printf("listen....\n");
struct sockaddr_in caddr; //保存客户端的ip及port
memset(&caddr, 0, sizeof(caddr));
socklen_t clen = sizeof(caddr);
#if 0
//int rws = accept(sockfd,NULL, NULL);//rws用于和客户端通信
#else
int rws = accept(sockfd, (struct sockaddr *)&caddr, &clen); // rws用于和客户端通信
if (-1 == rws)
{
perror("accept");
close(sockfd);
return -1;
}
#endif
printf("rws=%d\n", rws);
#define SIZE 128
char buf[SIZE];
//使用IO多路复用解决阻塞问题
struct pollfd pollfd[1024];
memset(pollfd, 0, sizeof(pollfd));
pollfd[0].fd = 0; //将标准输入添加到集合
pollfd[0].events = POLLIN; //指定文件描述符的读事件
pollfd[1].fd = rws; //将rws添加到集合
pollfd[1].events = POLLIN;
int count = 2; //指定监测的文件描述符的个数
while (1)
{
ret = poll(pollfd, count, -1);
if (-1 == ret)
{
perror("poll");
break;
}
if (pollfd[0].revents == POLLIN) //判断标准输入文件描述符是否准备就绪
{
memset(buf, 0, sizeof(buf));
fgets(buf, SIZE - 1, stdin); //读标准输入
ret = send(rws, buf, sizeof(buf), 0); //给客户端发送消息
if (-1 == ret)
{
perror("write");
break;
}
}
if (pollfd[1].revents == POLLIN) //判断rws是否准备就绪
{
memset(buf, 0, SIZE);
ret = recv(rws, buf, sizeof(buf), 0); //读取客户端发送的消息
if (-1 == ret)
{
perror("read");
break;
}
else if (0 == ret) //客户端关闭
{
printf("client closed\n");
close(pollfd[1].fd);
pollfd[1].fd = -1; //文件描述符指定为-1时,表示当前文件描述符无效
break;
}
else
fputs(buf, stdout);
}
}
close(sockfd);
return 0;
}
2.2.4 本地通信UNIX
服务器
(1) 创建套接字 – socket ( ) | ||
(2) 填充服务器本地信息结构体 – sockaddr_un | ||
(3) 将套接字与服务器本地结构体绑定 – bind ( ) | ||
(4) 将套接字设置为被动监听状态 – listen ( ) | ||
(5) 阻塞等待客户端的连接请求 – accept ( ) | ||
(6) 进行通信 – recv ( ) / send ( ) 或 read ( ) / write ( ) | ||
客户端
(1) 创建套接字 – socket ( ) | ||
(2) 填充服务器本地信息结构体 – sockaddr_un | ||
(3) 将套接字与服务器本地结构体绑定 – bind ( ) | ||
(4) 将套接字设置为被动监听状态 – listen ( ) | ||
(5) 阻塞等待客户端的连接请求 – accept ( ) | ||
(6) 进行通信 – recv ( ) / send ( ) 或 read ( ) / write ( ) | ||
2.2 UDP
2.2.1 广播服务器
发送者
setp | ||
---|---|---|
1 | 创建套接字 | socket ( ) |
2 | 设置为允许发送广播权限 | setsockopt ( ) |
3 | 填充广播信息结构体 | sockaddr_in |
4 | 发送数据 | sendto ( ) |
接收者
(1) 创建套接字 – socket ( ) | ||
(2) 填充广播信息结构体 – sockaddr_in | ||
(3) 将套接字与广播信息结构体绑定 – bind ( ) | ||
(4) 接收数据 – recvfrom ( ) | ||
2.2.2 组播服务器
发送者
(1) 创建套接字 – socket ( ) | ||
(2) 填充组播信息结构体 – sockaddr_in | ||
(3) 发送数据 – sendto ( ) | ||
接收者
(1) 创建套接字 – socket ( ) | ||
(2) 填充组播信息结构体 – sockaddr_in | ||
(3) 将套接字与组播信息结构体绑定 – bind ( ) | ||
(4) 设置为加入多播组 – setsockopt ( ) | ||
(5) 接收数据 – recvfrom ( ) |
三、轻量级数据库
3.1 安装sqlite3
sudo apt-get install sqlite3 #安装SQLite3
sudo apt-get install libsqlite3-dev #安装Sqlite3编译需要的工具包
sqlite3 -version #查看版本
sudo apt-get install sqlitebrowser #安装图形界面
sqlitebrowser #打开图形化界面
sudo dpkg -i *.deb #离线安装
3.2 系统命令
终端界面输入 sqlite3 进入
进入之后可正常输入命令,如果出现...>
的模式,用CTRL + D 结束,或者CTRL + Z。
以‘.’(点)开头的命令
.help 帮助
.quit 退出
.exit 退出
.schema 查看标的结构图
.databases 查看打开的数据库
.table 查看表
3.3 sql命令
不以
.
(点)开头,但是需要以;
(分号)结尾;
- 创建一张数据的表
create table student(no Integer,name char,score float);
数据类型:
Integer 整型
char 字符串,也可以用string
...
-
插入一条数据
完整数据插入:
insert into student values(1, ‘zhangsan', 80); insert into 表名 values(成员1, 成员2, 成员3, ...); /*字符串zhangsan既可以用单引号‘zhangsan’,也可以用双引号“zhangsan”;*/
部分数据插入
insert into student (no,name) values(2, 'lisi'); insert into student (字段1,字段2) values(字段1的值, 字段2的值);
-
查询记录
完整数据查询
select * from student;
部分数据查询
select no,name from student;
按照条件查询
select * from student where score=100; select * from student where no=1 and score=100; select * from student where no=1 or score=100;
-
删除记录
删除某一条记录
delete from student where name='lisi';
删除整张表数据
delete from student;
-
更新记录
update student set name=‘lisi’ where id=3; update student set name='lisi',score=80 where id=2; update student set 要修改的字段 where 要查询的字段;
-
在表中增加一列
alter table student add column address char
-
在表中删除一列(不支持直接删一列)
-
删除原有的表格
drop table student;
-
将新的表格名字改为原有表的名字
alter table stu rename to student;
-
创建一个新的表并从原有表中提取字段
create table stu as select id, name, score from student;
3.4 sqlite3函数接口
打开数据库
int sqlite3_open( /* 功能:打开一个数据库 */
const char *filename, /* Database filename (UTF-8) 要操作的数据库文件的路径;*/
sqlite3 **ppDb /* OUT: SQLite db handle */
);
{
@msg
*filename: 代表数据库的路径名;
**ppdb: 代表数据库的操作句柄(指针);
@return
成功-SQLITE_OK,出错-错误码;
}
关闭数据库
int sqlite3_close(sqlite3 *db); /* 功能:关闭数据库 */
{
@msg
db :操作数据库的句柄;
@return
成功-SQLITE_OK,出错-错误码;
}
获得操作的错误信息
const char *sqlite3_errmsg(sqlite3 *db); /* 功能:通过db句柄,得到数据库操作的错误信息 */
{
@msg
db :操作数据库的句柄;
@return
成功-错误信息的首地址,出错-错误码;
}
返回值:错误信息的首地址;
执行sql语句
int sqlite3_exec( /* 执行一条sql语句;*/
sqlite3*, /* An open database */
const char *sql, /* SQL to be evaluated */
int (*callback)(void*,int,char**,char**), /* Callback function */
void *arg, /* 1st argument to callback */
char **errmsg /* Error msg written here */
);
{
@msg:
db :数据库操作句柄;
sql :一条sql语句;
callback:只有sql为查询语句的时候,才会执行此语句;
arg :给回调函数callback传递参数;
errmsg :错误消息
@return
成功-SQLITE_OK,出错-错误码;
}
//实例
strcpy(sql, "create table student(no int, name char, score float);");
int ret = sqlite3_exec(db, sql, NULL, NULL, &errmsg);
if(ret != SQLITE_OK)
{
printf("create:%s\n", sqlite3_errmsg(db));
exit(-1);
}
回调函数
/* Callback function */
int (*callback)(void *parg, int f_num, char **f_value, char **f_name);
功能:查询的结果,是一个函数指针类型,传递一个函数名即可。
{
@msg
*para :传递给回调函数的参数;
f_num :记录中包含的字段数目;
**f_value :包含每个字段值的指针数组;
**f_name :包含每个字段名称的指针数组
@return
成功返回0,失败返回-1;
}
//回调函数示例
int callfun(void *parg, int f_num, char **f_value, char **f_name)
{
printf("----------------\n");
for(int i = 0;i<f_num;i++)
{
printf("%s:%s\n", f_name[i],f_value[i]);
}
return 0; //这句话很重要,不加就会出错
}
数据查询
查询数据:
int sqlite3_get_table(
sqlite3 *db, /* An open database */
const char *zSql, /* SQL to be evaluated */
char ***pazResult, /* Results of the query */
int *pnRow, /* Number of result rows written here */
int *pnColumn, /* Number of result columns written here */
char **pzErrmsg /* Error msg written here */
);
功能,查询表中数据
参数:
*db :数据库操作句柄
*zSql :查询的sql语句
***pazResult :查询结果的返回地址;
*pnRow :满足条件的记录数目(表中的行数)
*pnColumn:每条记录包含的字段数目(表中的列数)
**pzErrmsg:错误信息指针的地址;
返回值:成功返回0,失败返回错误码。
ret = sqlite3_get_table(db, cmd, &data, &row, &col, &errmsg);
//错误判断
//数据被存在data的二级指针指向的地址中,存放是将记录中的每个字段数据依次存放
void sqlite3_free_table(char **result);
查看错误信息
char *sqlite3_errmsg(sqlite3 *db)
{
@msg
*db :数据库操作句柄
@return
char *:错误信息
}
printf("create:%s\n", sqlite3_errmsg(db));
{
printf(“create:%s\n”, sqlite3_errmsg(db));
exit(-1);
}
> 回调函数
~~~c
/* Callback function */
int (*callback)(void *parg, int f_num, char **f_value, char **f_name);
功能:查询的结果,是一个函数指针类型,传递一个函数名即可。
{
@msg
*para :传递给回调函数的参数;
f_num :记录中包含的字段数目;
**f_value :包含每个字段值的指针数组;
**f_name :包含每个字段名称的指针数组
@return
成功返回0,失败返回-1;
}
//回调函数示例
int callfun(void *parg, int f_num, char **f_value, char **f_name)
{
printf("----------------\n");
for(int i = 0;i<f_num;i++)
{
printf("%s:%s\n", f_name[i],f_value[i]);
}
return 0; //这句话很重要,不加就会出错
}
数据查询
查询数据:
int sqlite3_get_table(
sqlite3 *db, /* An open database */
const char *zSql, /* SQL to be evaluated */
char ***pazResult, /* Results of the query */
int *pnRow, /* Number of result rows written here */
int *pnColumn, /* Number of result columns written here */
char **pzErrmsg /* Error msg written here */
);
功能,查询表中数据
参数:
*db :数据库操作句柄
*zSql :查询的sql语句
***pazResult :查询结果的返回地址;
*pnRow :满足条件的记录数目(表中的行数)
*pnColumn:每条记录包含的字段数目(表中的列数)
**pzErrmsg:错误信息指针的地址;
返回值:成功返回0,失败返回错误码。
ret = sqlite3_get_table(db, cmd, &data, &row, &col, &errmsg);
//错误判断
//数据被存在data的二级指针指向的地址中,存放是将记录中的每个字段数据依次存放
void sqlite3_free_table(char **result);
查看错误信息
char *sqlite3_errmsg(sqlite3 *db)
{
@msg
*db :数据库操作句柄
@return
char *:错误信息
}
printf("create:%s\n", sqlite3_errmsg(db));