分类 未分类 下的文章

数据库索引设计与优化02-BE与QUBE

前瞻性的索引设计

发现不合适的索引

一旦一个应用的明细方案确定下来,就应该确认当前的额索引对新的应用来说是否合适。为了完成这个工作,可以使用两种简单,快速并且可行的方法

  1. 基本问题法(Basic Question, BQ)
  2. 快速上线估算法(Quick Upper-Bound Estimate, QUBE)

基本问题法(BQ)

是否已有一个已存在的或者计划中的索引包含了where子句所引用的所有列(一个半宽索引)?

  • 如果答案是否,那么我们应该首先考虑将缺少的谓词列加到一个现有的索引上去。这将产生一个半宽索引,尽管索引的等值匹配过程并不令人满意(一星),但是索引过滤可以保证回表访问只发生在所有查询条件都满足的时候。
  • 如果这还没有达到足够的性能,那么下一个选择就是将所有涉及的列都加到索引上,以使访问路径只需要访问索引。这将产生一个避免所有表访问的宽索引。
  • 如果select仍然很慢,就应该用前面的候选索引算法来涉及一个新的索引。根据定义,这将是所能实现的最佳索引。

如何确认第一个方案(半宽索引)或第二个(宽索引)方案能否让select在最差输入下仍然运行的足够快?

如果可以访问生产库或者类似生产库的测试库,我们可以每次创建一个索引,用最差输入来测试响应时间。为了确保测得的响应时间接近于在正常生产库上允许的性能表现,我们必须把缓冲池考虑进来,并观察每个事务的缓冲池命中率。测试的第一个事务很可能在缓冲池中没有发现相应的缓存页,所以在最差输入下的磁盘读的指标会比正常环境要高。此外第一个访问索引的事务必须要等待文件被打开。而另一方面,如果变量的输入值保持不变,那么第二个访问改索引的事务将很可能会获得100%的缓冲池命中率(没有磁盘读)。为了获得具有代表性的响应时间,每个索引方案都应在进行过有预热事务后再开始测试,可以通过传入一些典型值(但不是最差情况的值)来打开文件,并将大部分非页子索引页加载到数据库的缓冲池中。这样,使用最差输入值的的事务就会有一个比较有代表性的响应时间了,至少我们已经把CPU和磁盘读的服务时间考虑进来了。

实际上,使用第二种方法,QUBE来评估索引方案将不会那么乏味。

如果模拟测试和QUBE方法都没有实施,那么就应当选择使用宽索引方案,并在应用切换到生成环境后立即启用异常报告来发现那些连宽索引都无法满足的性能要求的场景。如果必要的话,我们需要为那些运行缓慢的查询设计我们所能达到的最佳索引。

注意

对于BQ的一个肯定的回答并不能保证足够的性能。记住,BQ的目的是确保我们至少可以通过索引过滤来最小化对表的访问,除此之外没有其他作用。

举个例子,假设有一个select,谓词是where b = :b and c = :c,唯一有用的索引是(A, B, C)。这个索引包含了该select的所有谓词列,因此BQ方法检查这个select并不会产生告警。然而这个查询的访问路径将会是全表扫描。如果该表有超过100000条记录,那么查询将运行的非常慢。索引过滤本身并不意味着这个索引有三颗星中的任何一颗。

快速上限估算法(QUBE)

这个快速估算法输出的结果是本地响应时间(LRT),即在数据库服务器中的耗时。在单层环境(指发出SQL调用的应用与数据库部署在同一台机器上)中,一个传统事务的LRT是指用户和数据库服务器之间一次交互的响应时间,不包括所有的网络延迟。在多层环境(客户端/服务器)中。各层之间的通信时间也被排除在外。批量任务的LRT是指执行任务耗时。任何任务队列中的排队时间也被排除在外。

服务时间

在简单的场景下(I/O时间和CPU时间不重叠),服务时间等于CPU时间加上排除了磁盘驱动排队的随机读时间。如果没有资源竞争,则本地响应时间等于服务时间。

排队时间

在一个常规的多用户环境中,程序的并发会导致对所需资源的各种竞争,因此这些并发的程序不得不排队开获取这些资源。以下资源在LRT的范畴内 :

  • CPU排队时间(所有处理器都忙着处理更高优先级的任务)
  • 磁盘驱动器排队(包含请求页的驱动器处于繁忙状态)
  • 锁等待(请求的表或行被锁定在一个不兼容的级别)
  • 多道编程的级别,线程数或其他方面已经达到上限(这些都是为了防止资源过载而设计的系统值)

QUBE会忽略除磁盘驱动排队以外的其他所有类型的排队,以提供一个简单的评估过程,用于评估那些对性能影响特别大的方面。

在排除上述因素后,我们得到的是一个非常简单的估算过程,仅需两个输入变量,TR和TS,必要时还有第三个输入变量,F。这些就可以将SQL的处理及I/O成本考虑进来了,并且他们是影响索引设计的主要因素。如果是使用QUBE来比较多个可选访问路径之间的性能差异,由于FETCH调用的次数是相同的,所以可以忽略FETCH调用次数带来的影响。如果是使用QUBE来确定响应时间,那就需要包含FETCH调用的次数。

比较值
LRT = TR * 10ms + TS * 0.01ms

绝对值
LRT = TR * 10ms + TS * 0.01ms + F * 0.1ms

LRT = 本地响应时间
TR = 随机访问的数量
TS = 顺序访问的数量
F = 有效FETCH的数量

基本概念

访问

根据定义,DBMS读取一个索引行或一个表行的成本称为一次访问 : 索引访问或表访问。如果DBMS扫描索引或表的一个片段(被读取的行在物理上是彼此相邻的),那么第一行的读取即为一次随机访问。对于后续行的读取,每行都是一次顺序访问。在当前的硬件条件下,顺序访问的成本比随机访问的成本低得多。一次索引访问的成本与一次表访问的成本基本上是相同的。

读取一组连续的索引行

物理上彼此相邻是什么意思?

索引上的所有行都通过指针链接在一起,链接的先后顺序由索引的键值严格定义。当几个索引行的键值相同时,就根据索引行存储的指针值进行链接。在传统的索引设计(从某个角度看,是理想化的)中,链表从LP1(叶子页1)开始,随后链接LP2,以此类推。这样(假设每个磁道可以放12个叶子页,当前的硬件通常可以容纳更多),叶子页就组成了一个连续的文件,LP1至LP12存储在磁盘柱面的第一个磁道,LP13至LP24存储在下一个磁道,如此继续,当第一个柱面存满后,下一组LP就会被存储在下一个柱面的首个磁道上。换句话说,就是叶子页之间没有其他页。

现在,读取一个连续的索引行(即一个索引片,或者包含了单个键值或者一个范围的键值所对应的索引行)就非常快了。一次磁盘旋转会将多个叶子页读取进内存中,而且只有在磁盘指针移到下一个柱面时才需要进行一次短暂的寻址、

不过,这个完美的顺序还是会被打破的,至少有以下三个影响因素 :

  1. 如果一个叶子页没有足够的空间存储新插入的索引行,那么叶子页就必须被分裂。之后链表仍会按照正确的顺序链接索引行,但是这与底层的物理存储顺序就不再一致了,一些按道理应该是顺序的访问就变成随机访问了。不过索引的充足可以再次恢复最理想的顺序。
  2. 意向不到的数据增长可能会填满原本连续的空间(区或类似的概念)。操作系统于是就会寻找另外有一个连续的空间,并将它连接到原来空间的后面。这时候从第一个区跨到第二个区访问就会产生一次随机访问,不过这种情况影响不大。
  3. RAID 5条带会将前几个叶子页存储在一个驱动器上,将后面的叶子页存放在另外的驱动器上。这就会产生额外的随机读,但实际上条带的积极作用要大过随机读带来的性能恶化,一个智能的磁盘服务器可以将后续的叶子页并行的从多个驱动器上读取至磁盘缓存中,从而大大降低了单个叶子页的I/O时间。此外,在RAID 5条带策略下,一个被频繁访问的索引的不太可能导致某一个磁盘负载过高,因为I/O请求会被均匀低分布到RAID 5阵列内的多个磁盘驱动器。

忽略上述情况,我们仍然假设,如果两个索引行在链表上彼此相邻(或者在唯一索引中,相同键值的行指针意味着彼此相邻),那么我们就认为这两行在物理上也相邻。这就意味着QUBE认为所有的索引都有最理想的顺序。

读取一组连续的表行

读取一组连续的表行有如下两种情况。

  1. 全部扫描
    从TP1(表页1)开始,读取该页上所有的记录,然后再访问TP2,一次类推。按照记录在表页中存储的顺序进行读取,没有其他特殊的顺序。
  2. 聚簇索引扫描
    读取索引片上第一个索引行,然后获取相应的表行,再访问第二个索引行,以此类推。如果索引行与对应的表行记录顺序完全一致(聚簇率为100%),那么除了第一次之外的所有表访问就都是顺序访问。表记录的链接方式跟索引不一样。单个表页中记录的顺序无关紧要,只要访问的下一个表记录在同一个表页或者相邻的下一个表页内就可以了。

同索引一样,存储表的传统方式也是将所有表页保留在一个连续的空间内。引起顺序杂乱或碎片化的因素也和索引中的相似,但又两个地方不同

  1. 如果往表中插入的记录在聚簇索引所定义的主页中装不下,则通常不会移动现有的行,而是会将新插入的记录存储到离主页尽可能近的表页中。对第二个页的随机I/O会使聚簇索引扫描变得更慢,但是如果这条记录离主页很近,这些额外的开销就可以被避免,因为顺预读功能会一次性将多个表页装载到数据库缓存中。即使顺序预读功能没有使用,也只有当该页在数据库缓存被覆盖的情况下才会发生额外的随机I/O。
  2. 一条记录被更新后,可能因为表行过长导致其无法再存储于当前的表页中。这是DBMA就必须将该行记录迁移至另外一个表页中,同时在原有的表页中存储指向新表页的指针。当该行被访问时,会引入额外的随机访问。

表可以通过重组来还原行记录的顺序,从而减少不必要的随机访问。因此,如果行记录存储在同一个页或者相邻的页当中,QUBE就认为他们在物理上彼此相邻。换句话说,QUBE假设所有的表,所有都是以最理想的顺序组织的。

之前我们做过一些最差场景的的假设,在这种假设下,QUBE足够简单,能够快速且方便的使用。同样的原因,我们也有必要做一些乐观的假设。尽管存在上述问题,根据QUBE的定义,扫描索引或表的一个片段只需进行一次随机访问。数据库专家们需要对重组的必要性进行监控,以保证我们所做的这些乐观假设都是合理的。

计算访问次数

既然访问对于QUBE而言如此重要,我们现在就解释下如何确定索引及表的访问次数,包括随机访问和顺序访问。

随机访问

我们首先思考一下磁盘读与访问的区别。一次磁盘读所访问的对象是一个页,而一次访问的访问对象则是一行。一次随机磁盘读会将一整页(通常会包含很多行)读取至数据库的缓冲池中,但是根据定义,前后两次随机读不太可能会访问到同一个页。因此,QUBE中单次随机访问所消耗的时间与一次磁盘随机读的平均耗时是一样的,都是10ms。虽然随机读是同步的,但是由于现在的处理器速度非常快,所以在估算单次随机访问开销时可以忽略CPU时间,这一CPU时间通常小于0.1ms。

顺序访问

一次顺序访问是指读取物理上连续的下一行,这一行要么存储在同一页中,要么在下一页中。由于顺序读的CPU时间与I/O时间是重叠的(DBMS和磁盘控制器通常会预读一些页),因此顺序访问的消耗时间就是两者中较大的那个。在QUBE中,一次顺序读所消耗的时间是0.01ms。

当计算访问次数时,为了简单起见,我们会遵循以下规则:

  1. 忽略索引的非叶子节点。我们假定他们都在数据库的缓冲池中,或者至少在磁盘服务器的读缓存中。
  2. 假设DBMS能够直接定位到索引片的第一行(忽略为了定位索引行的位置而使用二分查找或者其他技术所耗费的时间)。
  3. 忽略跳跃使顺序读所节省的任何时间。这可能是一个很悲观的假设。
  4. 假设所有的索引和表都处于理想的组织顺序。如上所述,这可能是个很乐观的假设,除非表和索引都被很好的监控着。

当计算索引访问次数时,可以将索引看成一个微表,他的行数与其指向的表包含的行数相同,且按照索引键值排列。

当计算表访问次数时,我们假设表行在表页中是按理想顺序排序的,这个顺序依赖于表的组织方式。我们可以假设一次全表扫描(N行数据)将需要一次随机访问和N-1次顺序访问。

FETCH处理

被接受的行的数量可以通过FETCH调用的次数来确定(除非多行FETCH可用),这些行将经历更多的处理程序。TS不包含这个额外的处理过程。现在,我们需要将第三个输入变量,LRT组成中的F考虑进来。F的成本比TS大一个数量级,而另一方面,他比TR的成本要小很多。

如果QUBE被用来比较可选的访问路径,比如比较全表扫描与使用特定索引,那么F参数是无关紧要的,因为他的值在两种情况下相等,我们只需要考虑TR和TS。但如果使用QUBE来确定LRT,F参数就可能会比较重要,这取决于FETCH调用的次数。

在处理被接受了的行时,可能会涉及排序操作,排序的整体开销通常与FETCH的行数成正比。大多数情况下,相对于在所接受的行上进行其他处理过程,排序的开销非常小,所以他被隐藏在F的开销内。然后有些场景却不是这样,这个将会在后面讨论。除了这些例外场景外,我们都假定排序开销包含在F参数中。

请注意,在计算FETCH调用次数时,为了避免混淆,我们将忽略“判断没有更多符合条件的行”的那次调用,这纯粹是为了简化计算。例如,一张有10000条计算的表上的一个过滤因子为1%,期望的返回行数是100,那么我们就假设F为100而不是101.

主要访问路径的QUBE示例

示例5.1 : 主键索引访问

SELECT CNO,LNAME,FNAME
FROM
WHERE CNO=:CNO

通过主键索引读取一个表行需要随机访问一次表和随机访问一次索引。

索引 CNO         TR = 1
表   CUST        TR = 1
提取 1 * 0.1ms
LRT        TR = 2
           2 * 10ms + 0.1ms ≈ 20ms

示例5.2 : 聚簇索引访问

SELECT CNO,LNAME,FNAME
FROM CUST
WHERE ZIP = :ZIP AND LNAME = :LNAME
ORDER BY FNAME

假设区号为(ZIP)30103的地区内有1000位名为Joneses的客户,通过这个二星索引的两个匹配列访问了一个薄的索引片。这里不需要排序,因为索引已经提供了所需的顺序。

扫描1000行的索引片需要多长时间?首先,需要进行一次随机访问来找到索引片上的第一条符合条件的索引行,然后DBMS继续向前读取,知道找到列ZIP和LNAME与输入值不匹配的行为止。包括访问的最后一个不匹配的行,索引的访问数是1000。因此,根据QUBE,扫描索引片需要1 10ms + 1000 0.01ms = 20ms。

因为列CNO不在索引中,所以必须从表中读取该列。由于索引是聚簇的,而且我们假设表中的1000行是相邻的,于是扫描表片段的成本将会是1 10ms + 999 0.01ms ≈ 20ms。此外,还有FETCH调用的时间,根据QUBE,总的响应时间为140ms。由于顺序访问的成本非常低,因此表的访问成本是非常低的。最大的开销来自DETCH调用的过程。

索引 ZIP,LNAME,FNAME TR = 1      TS = 1000
表   CUST = 1                    TS = 999
提取 1000 * 0.1ms

LRT     TR = 2      TS = 1999
        2 * 10ms    1999 * 0.01ms
        20ms + 20ms + 100ms = 140ms
        20ms + 20ms + 1000 * 0.1ms
        20ms + 20ms + 100ms = 140ms

示例5.3 : 非聚簇的索引访问

SELECT CNO,LNAME,FNAME
FROM CUST
WHERE ZIP = :ZIP AND LNAME = :LNAME
ORDER BY FNAME

如果表的记录不是通过聚簇索引访问的,那么表的1000次访问就会变成随机访问。

索引      ZIP,LNAME,FNAME      TR = 1      TS = 1000
表        CUST                 TR = 1000   TS = 0
提取      1000 * 0.1ms
LRT                            TR = 1001   TS = 1000
                               1001 * 10ms 1000 * 0.1ms
                               10s + 10ms + 1009 * 0.1ms ≈ 10s

在这种情况下,将列CNO加到索引中让索引称为三星索引会是个好方案,因为这样能使响应时间从10s降低至0.1s

索引      ZIP,LNAME,FNAME      TR = 1      TS = 1000
表        CUST                 TR = 0      TS = 0
提取      1000 * 0.1ms
LRT                            TR = 11   TS = 1000
                               1 * 10ms 1000 * 0.1ms
                               10ms + 10ms + 1009 * 0.1ms ≈ 120ms

示例 5.4 : 使用聚簇索引进行跳跃式顺序表访问

SELECT STREET, NUMBER, ZIP, BORN
FROM CUST
WHERE LNAME = 'JONES' AND FNAME = 'ADAM' AND CITY = 'LONDON'
ORDER BY BORN

5.4的查询将使用聚簇索引(LNAME,FNAME,BORN,CITY)。在这一索引条件下,要找到住在伦敦的所有名为Adam Joneses的客户,需要多少次随机访问和顺序访问呢?
假设LNAME和FNAME匹配之后的索引片大小为5

索引      LNAME,FNAME,BORN,CITY      TR = 1      TS = 4
表        CUST                       TR = 2      TS = 0
提取      2 * 0.1ms
LRT                                  TR = 3      TS = 4
                                     3 * 10ms    4 * 0.1ms
                                     40ms + 0.4ms + 2 * 0.1ms ≈ 30.4ms

使用满足需求的成本最低的索引还是所能达到的最有索引

实例1

我们已经通过一些例子展示了如何运用QUBE来方便的评估一些比较重要的访问方式,现在我们将展示如何使用这两种技术(BQ和QUBE)来完成整个索引设计过程。

假设有一张表,结构如下

create table `cust`(
`cno` varchar(8) primary key,
`pno` varchar(8) not null default '',
`fname` varchar(8) not null default '',
`lname` varchar(8) not null default '',
`city` varchar(8) not null default '',
key `idx_pno`(`pno`),
key `idx_lname_fname`(`lname`, `fname`)
)

再假设下其他的条件

  1. 表中有100万记录
  2. idx_lname_fname是唯一合适的索引
  3. 谓词lname = :lname的最大过滤因子是1%
  4. 谓词city = :city的最大过滤因子是10%
  5. 结果集最多包含10000条记录(1000000 1% 10%)
  6. 表中的记录按照cno列的大小顺序存储,主键索引cno是聚簇索引,或者表经常按照cno进行排序重组。

sql语句如下

select cno,fname
from cust
where lname = :lname and city = :city
order by fname
该事务的基本问题

是否有一个现有的或者计划中的索引包含了where子句中所涉及的所有列?

换句话说,我们是否现在或者即将有一个半宽索引?答案显示是没有,至少从现有的索引情况看是这样。这并不一定意味着存在问题,性能还是有可能令人满意的。但至少这是一个警示信号,我们应该去思考是否真的存在问题,如果是并且我们决定忽略它,那么它会变得多严重?我们是否需要将那些不在索引中的谓词列加到索引中?这样做将给BQ一个肯定的答案,但我们应该牢记这仍然不能确保良好的性能。

对事务上限的快速估算

当DBMS选择使用索引idx_lname_fname时,结果行是按照所请求的顺序(order by fname)被获取的,既不需要排序。因此,DBMS将不会进行早期物化 : OPEN CURSOR动作不会产生对表或索引的访问,而是在每次FETCH的时候访问索引和表,其中的一些访问会将记录行返回给FETCH,另一些(大部分)则不会。整个过程将引起一次所有扫描,匹配列只有一个,该列的过滤因子是1%。根据QUBE

索引      LNAME,FNAME      TR = 1      TS = 9999
表        CUST             TR = 10000   TS = 0
提取      1000 * 0.1ms
LRT                        TR = 10001  TS = 9999
                           10001 * 10ms    9999 * 0.1ms
                           100s + 1s + 1000 * 0.1ms ≈ 101.1s

很明显,这里有一个很明显的问题,即数据库必须读取10000条不相邻的表记录。这需要耗费很长时间,将近2分钟。

QUBE是一个对上限的估算法 : 结果100s意味着本地响应时间最多会达到100s。真实的LRT可能比这个值小,特别是当内存和缓冲池的大小对数据库来说非常充足时。这种情况下,许多随机访问将不再需要从磁盘驱动器上读取,随机访问的平均响应时间也可能比10ms低。不管怎么说,这不是一个错误的告警,对表的访问次数太高了。

由于QUBE是一个非常粗略的工时,因此在结果中显示多于一位的有效数字是具有误导性的,尽管为了清除起见我们会这么做。在真实的报告中,应该这样阐述结论 : 根据快速估算,被提议的索引能将最差情况的响应时间从2min降至1s。

使用满足需求的成本最低的索引还是所能达到的最有索引

设计索引并不只是单纯的为了最小化磁盘I/O的总量,而是为了设法让所有程序都运行的足够快,同时还能做到不使用过量的磁盘存储,内存和读缓存,且不使磁盘超载。

我们甚至可以将这种决策以数字的方式来表达,比如这样问 : 将一个每月运行50000次的事务的响应时间从2s~10s降低至0.1s~0.5s,同时节省1000秒的CPU运算时间,为达到这样的目的每个月支付10美元是否值得?

通过BQ,QUBE(或者其他方法),我们已经发现了这样一个事实 : 改SQL会由于没有合适的索引执行的非常慢。现在,我们面临一个困难的问题 : 我们应该给这个语句设计一个所能达到的最优索引,还是为期设计一个成本最低的满足性能要求的索引?又或是介于两者之间的某种索引?我们并没有一个硬性公式去解答这个问题。

该事务的最佳索引

使用前面描述的算法可以得到两个三星索引
(lname, city, fname, cno)或(city, lname, fname, cno)
这两个索引都有三颗星,因为他们会扫描一个非常薄的索引片,order by列跟在匹配列之后,从而避免了排序;此外,此查询只需要访问索引而无需回表。

索引      lname, city, fname, cno      TR = 1      TS = 999
表        CUST                         TR = 0      TS = 0
提取      1000 * 0.1ms
LRT                                    TR = 1      TS = 999
                                       1 * 10ms    999 * 0.01ms
                                       10ms + 10ms + 1000 * 0.1ms ≈ 120ms

在性能方面,这两个索引并没有什么区别,他们都极大的降低了成本。不幸的实际,三星索引将意味着额外增加一个索引,因为列(city)添加到现有索引的lname和fname之间可能会对现有的程序造成不利的影响。这就是为什么我们可以考虑一下开销更低的可选方案。

半宽索引(最大化索引过滤)

将谓词列city加至现有索引(lname,fname)的末端,可以消除大部分的表访问,因为会引入索引过滤过程。表中的行只有在确定包含所请求的city值的情况下才会被访问。

该索引(lname, fname, city)是满足BQ的,所有谓词列都在索引中。然而,他只有一颗星,所请求的索引行不是连续的,当然,他也不是宽索引。但是这种开销很低的方案是否满足需求了呢?

索引      lname, fname, city      TR = 1      TS = 9999
表        CUST                    TR = 1000   TS = 0
提取      1000 * 0.1ms
LRT                               TR = 1001   TS = 9999
                                  1001 * 10ms 9999 * 0.01ms
                                  10s + 100ms + 1000 * 0.1ms ≈ 10.2s

借助于这个半宽索引,LRT由原来的近2min降低到了10s,这是一个巨大的进步,但还不够(别忘了最佳索引下的运行时间仅为120ms)。我们仍有太多成本很高的TR,我们需要一个宽索引,已取得更好的性能。

宽索引(只需访问索引)

将一个列添加到半宽索引上,将使改索引变为一个宽索引 : (lname, fname, city, cno)。现在,我们有了两颗星,第二颗和第三颗,同时LRT变为了1s。

索引      lname, fname, city, cno      TR = 1      TS = 9999
表        CUST                         TR = 0      TS = 0
提取      1000 * 0.1ms
LRT                                    TR = 1      TS = 9999
                                       1 * 10ms 9999 * 0.01ms
                                       10ms + 100ms + 1000 * 0.1ms ≈ 210ms

下图提供了各种索引的性能比较,最后一列显示的是由此引起的额外的维护成本。需要注意的是,三星所以将是一个新的索引。对于这些成本提醒如下两点事项 :

  1. 插入和删除意味着修改一个叶子页。如果该子叶不在缓冲池或者读缓存中,那么整个叶子页就必须从磁盘驱动器上读取,无论锁添加或删除的索引行的长度是多少,这都将耗费10ms的时间
  2. 更新city列会导致响应时间增加10ms或20ms,具体的值取决于是否需要将索引行迁移至另一个叶子页。通过将cust列作为最后一个索引列的方式可以避免这一移动。
类型索引LRT维护成本
现有索引lname,fname100s-
半宽索引lname,fname,city10sU city + 10 - 20ms
现有索引lname,fname,city,cno0.2sU city + 10 - 20ms
现有索引lname,city,fname,cno0.1sI/D + 10ms U + 10 - 20ms

LRT是最差输入下的QUBE值,I = 插入,D = 删除,U = 更新

重要说明

在这个阶段,我们需要将所有变更的维护成本也考虑进来。比如,在将索引升级为宽索引时,理论上是将cno添加至索引的末尾,但实际上,将它放在city列的前面会更好,即(lname, fname, cno, city)。对于这个select而言,由于city列参与的是索引过滤过程,所以他的位置并不会对运行结果造成影响。

同样,当有多个等值谓词作为匹配列时,我们需要考虑这些列在索引上的先后顺序。经常变化的列应当尽可能的排在后面(相对于lname,用户更改地址的可能性更大,因此lname,city可能是比较适合的顺序)。另一方面,我们需要考虑新索引对于其他select的帮助有多大。在本例中,我们已经有一个lname开头的索引了,因此从这个角度看,city,lanme可能更合适。显然,我们需要在这两者之间进行权衡,对于这个案例而言。后者可能更重要,因为city并不是一个频繁更新的列

更改现有索引列的顺序和在现有索引列之间添加新列同样危险。在这两种情况下,现有的select的执行速度都可能会急剧下降,因为匹配列减少了,或者引入了排序(导致过早产生结果集)

实例2

范围事务的BQ即QUBE

假设有一张表,结构如下

create table `cust`(
`cno` varchar(8) primary key,
`pno` varchar(8) not null default '',
`fname` varchar(8) not null default '',
`lname` varchar(8) not null default '',
`city` varchar(8) not null default '',
key `idx_pno`(`pno`),
key `idx_city`(`city`),
key `idx_lname_fname`(`lname`, `fname`)
)

再假设下其他的条件

  1. 表中有100万记录
  2. idx_lname_fname是唯一合适的索引
  3. 谓词lname = :lname的最大过滤因子是10%
  4. 谓词city = :city的最大过滤因子是10%
  5. 结果集最多包含10000条记录(1000000 10% 10%)
  6. 表中的记录按照cno列的大小顺序存储,主键索引cno是聚簇索引,或者表经常按照cno进行排序重组。

sql语句如下

select cno,fname
from cust
where city = :city and lname between :lname1 and :lname2
order by fname

idx_city与idx_lname_fname都不是半宽索引,所以两者都不满足BQ。QUBE的结果显示出二者的性能都非常差,无论使用哪个索引其结果都是相同的 : 两个索引的匹配扫描过程都只能使用1个MC,都需要进行排序

索引      lname, fname或city            TR = 1      TS = 99999
表        CUST                         TR = 100000      TS = 0
提取      10000 * 0.1ms
LRT                                    TR = 100001      TS = 9999
                                       100001 * 10ms 99999 * 0.01ms
                                       1000s + 1s + 10000 * 0.1ms ≈ 1002s
该事务的最佳索引

最佳索引很容易得到,候选方案A是
(city, lname, fname, cno)
该索引只有两颗星,因为需要排序(order by跟在范围谓词列的后面)。因此,候选方案B一定是(city,fname,lname,cno),他也只有两颗星,不过他具有的是第二颗星而不是第一颗。通过QUBE很容易判断出哪一个才是最佳索引。

候选索引A
索引      city, lname, fname, cno            TR = 1      TS = 9999
表        CUST                               TR = 0      TS = 0
提取      10000 * 0.1ms
LRT                                          TR = 1      TS = 9999
                                             1 * 10ms 9999 * 0.01ms
                                             10ms + 100ms + 10000 * 0.1ms ≈ 1110ms
候选索引B
索引      city, fname, lname, cno            TR = 1      TS = 99999
表        CUST                               TR = 0      TS = 0
提取      10000 * 0.1ms
LRT                                          TR = 1      TS = 99999
                                             1 * 10ms    99999 * 0.01ms
                                             10ms + 1000ms + 10000 * 0.1ms ≈ 2010ms

最佳索引显然是A,(city, lname, fname, cno),LRT为1s,但即使是最佳索引,事务的性能也一般。

我们早先提出过,如果只是为了比较不同的访问路径,fetch的处理可以被忽略,因为它在所有的情况下均是相同的。但是这么做之后,在决定选取哪个索引时要小心一点。例如,在这个案例中,如果忽略fetch的处理,那么候选方案A的成本仅为候选方案B成本的1/10,而实际的优势比例要小得多。

在这个例子中,候选方案A所需要的排序成本与候选方案B所节省的1秒相比是微不足道的,如前所述,事实上排序成本已经被包含在fetch的开销中了。候选方案B的问题在于我们使用的索引片更厚(10%),从而产生了大量的TS。

半宽索引(最大化索引过滤)

在两个现有索引的末端添加缺少的谓词列可以消除大量的随机访问,因为这样能引入索引过滤过程。只有在确定索引行中同时包含所需city和lname值时,表中的行才会被访问。

在现有索引的基础上,我们可以使用两个半宽索引
(city, lname)或(lname, fname, city)
我们再一次使用QUBE来判断那个是最好的。

索引      city, lname            TR = 1      TS = 9999
表        CUST                   TR = 10000      TS = 0
提取      10000 * 0.1ms
LRT                              TR = 10001      TS = 999
                                 10001 * 10ms 9999 * 0.01ms
                                 100s + 100ms + 10000 * 0.1ms ≈ 101s
索引      lname, fname, city            TR = 1      TS = 99999
表        CUST                          TR = 10000  TS = 0
提取      10000 * 0.1ms
LRT                                    TR = 10001   TS = 99999
                                       10001 * 10ms 99999 * 0.01ms
                                       100s + 1s + 10000 * 0.1ms ≈ 1002s

虽然第二个索引会有一个更厚的索引片,但这个因素在很大程度上被大量的表访问掩盖了(尽管已经通过过滤的方式将表访问的次数大大减少了)。看来我们需要设计一个宽索引来消除对表的10000次TR了。

宽索引(只需访问索引)

我们在上面评估的第一个宽索引上再多加两个列,在第二个索引上再多加一个列,两者就都变成了宽索引 : (city, lname, fname, cno)或(lname, fname, city, cno)

索引      city, lname, fname, cno            TR = 1      TS = 9999
表        CUST                               TR = 0      TS = 0
提取      10000 * 0.1ms
LRT                                          TR = 10001  TS = 999
                                             1 * 10ms 9999 * 0.01ms
                                             10ms + 100ms + 10000 * 0.1ms ≈ 1.11s
索引      lname, fname, city, cno            TR = 1      TS = 99999
表        CUST                               TR = 0      TS = 0
提取      10000 * 0.1ms
LRT                                          TR = 11     TS = 99999
                                             10001 * 10ms 99999 * 0.01ms
                                             10ms + 1s + 10000 * 0.1ms ≈ 2.01s

现在表的访问已经消除了,两个索引的LRT之间的区别就变得很明显了。第一个索引满足了第一颗星,即提供了一个薄的索引片,因为它所基于的原始索引只包含city列,这是在select中唯一的等值条件。正因为如此,我们才能将它升级为最佳索引,首先添加between谓词列(使其变为半宽索引,但仍不够),然后再添加select中引用的其他列(使其变为宽索引)。

类型索引LRT维护成本
现有索引lname或city17min-
半宽索引city, lname101sU lname + 10 - 20ms
半宽索引lname, fname, city102sU city + 10 - 20ms
宽索引city, lname, fname, cno1sU L & FNAME + 10 - 20ms
宽索引lname, fname, city, cno2sU city + 10 - 20ms
三星索引city, lname, fname, cno17minU L & FNAME + 10 - 20ms
三星索引city, fname, lname, cno17minU L & FNAME + 10 - 20ms

LRT是最差输入下的QUBE值,I = 插入,D = 删除,U = 更新

何时使用QUBE

理想情况下,QUBE应当在新方案的设计过程中使用。如果在当前或者计划设计的索引条件下最差输入的本地响应时间过长,比如大于2s,那么就应当考虑进行索引优化了。

尽管批处理任务的告警阈值视具体的任务而定,但检查一下所有批处理程序相邻提交点之间的最大时间间隔是很重要的,这个值也许应当小于1s或2s,否则该批处理任务有可能会导致事务池和其他批处理任务中的大量锁等待。

QUBE甚至可以在设计程序之前就开始使用,只需要简单地知道数据库一定会处理的最差输入场景即可。例如,读取表中10行不相邻的记录,像B表重增加2行相邻的记录,等等。事实上,直到估算结果令人满意时再开始设计程序是非常明智的做法。有时,我们可能还必须设计较为复杂的程序结构以满足性能的要求。

MySQL技术内幕(InnoDB存储引擎概述)学习笔记04-事务

概述

事务(Transaction)是数据库区别于文件系统的重要特性之一。在文件系统中,如果正在写文件,但是操作系统突然崩溃了,这个文件就很可能被破坏。当然有一些机制可以把文件恢复到某个时间点。不过,如果需要保证两个文件同步,这些文件系统可能就无能为力了。例如,在需要更新两个文件时,更新完一个文件后,在更新完第二个文件之前系统重启了,就会有两个不同的文件。

这正是数据库系统引入事务的主要目的 : 事务会把数据库从一种一致状态转换成另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有的修改都不保存。

InnoDB中的事务完全符合ACID特性。

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔离性(isolation)
  • 持久性(durability)

本章主要关注事务的原子性这一概念,并说明正确使用事务即编写正确的事务应用程序,避免在事务方面养成一些不好的习惯。

认识事务

事务可由一条简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中数据项的一个程序执行单元。在事务中的操作,要么都成功,要么都不成功,这就是事务的目的,也是事务模型区别于文件系统模型的重要特征之一。

理论上说,事务有着极其严格的定义,他必须同时满足四个特性,即通常所说的事务的ACID特性。值得注意的是,虽然理论上定义了严格的事务要求,但是数据库厂商处于各种目的,并没有严格的去满足ACID标准。例如,对于MySQL的NDB Cluster引擎来说,虽然其支持事务,但是不满足D的要求,即持久性的要求。对于Oracle数据库来说,其默认的事务隔离级别为READ COMMITTED,不满足I的要求,即隔离性的要求。虽然在大多数情况下,这并不会导致严重的后果,甚至可能带来性能的提升,但是用户首先需要知道严谨的事务标准,并在实际的生产应用中避免可能存在的潜在问题。对于InnoDB存储引擎而言,其默认的事务隔离级别为READ REPEATABLE,完全遵循和满足ACID特性。下面具体介绍下事务的ACID特性,并给出相关概念。

A(Atomicity),原子性。在计算机系统中,每个人都将原子性视为理所当然。例如在C语言中调用SQRT函数,要么返回正确的平凡根值,要么返回错误的代码,而不会在不可预知的情况下改变任何的数据结构和参数。如果SQRT函数被许多个程序调用,一个程序的返回值也不会是其他程序要计算的平方根。

然而在数据的事务中实现调用操作的原子性,就不是那么理所当然了。例如用户在ATM机前取款,假设取款的流程为

  1. 登录ATM机,验证密码
  2. 从远程银行的数据库中取得账户的信息
  3. 用户在ATM机上输入欲取出的金额
  4. 从远程银行的数据库中更新账户信息
  5. ATM机出款
  6. 用户取钱

整个取款的操作应该视为原子操作,即要么都做,要么都不做。不能用户钱未从ATM机上获得,但是银行卡上的钱已经被扣除了。

原子性是整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算成功。事务中任何一个SQL语句失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

如果事务中的操作都是只读的,要保持原子性其实是很简单的。一旦发生任何错误,要么重试,要么返回错误代码。因为只读操作不会改变系统中的任何相关部分。但是当事务中的操作需要改变系统中的状态时,例如插入或更新记录,那么情况就不一样了。如果操作失败,很有可能会引起状态变化,因此必须保护系统中并发用户访问受影响的部分数据。

C(Consistency),一致性。一致性是指事务将数据库从一种状态转换为下一种一致的状态。在事务开始之前和事务结束之后,数据的完整性约束没有被破坏。例如表中有一个字段为姓名,为唯一约束。如果一个事务对姓名字段进行了修改,但是事务在提交或事务操作发生回滚后,表中的姓名变得非唯一了,这就破坏了事务的唯一性要求,即事务将数据库从一种状态变为了另一种不一致的状态。因此,事务是一致性的单位,如果是事务中某个动作失败了,系统可以自动撤销事务,返回初始化的状态。

I(isolation),隔离性。隔离性还有其他的称呼,如并发控制(concurrency control),可串行化(serializability),锁(locking)等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以此来提高事务之前的并发度。

D(durability),持久性。事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。需要注意的是,只能从事务本身的角度来保证结果的永久性。例如,事务提交后,所有的变化都是永久的。即使当数据库 因为崩溃而需要恢复时,也能保证恢复后提交的数据都不会丢失。但若不是数据库本身发生故障,而是一些外部原因,如RAID卡损坏,自然灾害等原因导致数据库发生问题,那么所有提交的数据可能都会丢失。因此持久性保证事务系统的高可靠性(High Reliability),而不是高可用性(High Availability)。对于高可用性的实现,事务本身并不能保证,需要一些系统共同配合完整。

分类

从事务理论的角度来说,可以把事务分为以下几种类型

  • 扁平事务(Flat Transactions)
  • 带有保存点的扁平事务(Flat Transaction with Savepoints)
  • 链事务(Chained Transactions)
  • 嵌套事务(Nested Transactions)
  • 分布式事务(Distributed Transactions)

扁平事务是事务中最简单的一种,但是在实际生产环境中可能是最频繁的事务。在扁平事务中,所有操作都处于统一层次,由begin work开始,由commit work或rollback work结束,其间的操作是原子的,要么都执行,要么都回滚。因此扁平事务是应用程序称为院子操作的基本组成模块。

因为其简单,故基本每个数据库系统都实现了对扁平事务的支持。

扁平事务的主要限制是不能提交或回滚事务的某一部分,或分几个步骤提交。

带有保存点的扁平事务,除了支持扁平事务的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都失效,当其整个事务不合乎要求,开销也太大。保存点(Savepoints)用来通知系统应该记住事务当前的状态以便之后发生错误时,事务能回到保存点当时的状态。

对于扁平的事务来说,其隐式的设置了一个保存点。然后再整个事务中,只有这一个保存点,因此,回滚只能回滚到事务开始的状态。保存点用save work函数来建立通知系统记录当前的处理状态。当出现问题时,保存点能用作内部的重启动点,根据应用逻辑,决定是回滚到最近一个保存点还是其他更早的保存点。

链事务可视为保存点模式的一种变种。带有保存点的扁平事务,当发生系统崩溃时,所有保存点都将消失,因为其保存点是易失(volatile)的而非持久的(persistent)。这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。

链事务的思想是 : 在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。

链事务与带有保存点的扁平事务不同的是,带有保存点的扁平事务能回滚到任意正确的保存点。而链事务中的回滚仅限于当前事务,即只能恢复到最近的一个保存点。对于锁的处理,两者也不同。链事务在执行commit后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。

嵌套事务是一个层次结构框架。由一个顶层事务(top level transaction)控制着每个层次的事务。顶层事务之下前台的事务被称为子事务(substransaction),其控制每一个局部的变换

下面给出Moss对前台事务的定义

  1. 嵌套事务是由若干事务组成的一棵树,子树既可以是前台事务,也可以是扁平事务。
  2. 处在叶节点的事务是扁平事务。但是每个子事务从根到叶节点的距离可以是不同的。
  3. 位于根节点的事务称为顶层事务,其他事务称为子事务。事务的前驱(predecessor)称为父事务(parent),事务的下一层称为儿子事务(child)。
  4. 子事务既可以提交也可以回滚。但是他的提交操作并不马上生效,除非其父事务已经提交。因此可以推论出,任何子事务都在顶层事务提交后才真正提交。
  5. 树中的任意一个事务的回滚会引起他的所有子事务一同回滚,故子事务仅保留A,C,I特性,不具有D特性。

在Moss的理论中,实际的工作是交由叶子节点来完成的,即只有叶子节点的事务才能访问数据库,发送消息,获取其他类型资源。而高层的事务仅负责逻辑控制,决定何时调用相关的子事务。即使一个系统不支持嵌套事务,用户也可以通过保存点技术来模拟嵌套事务。

使用保存点技术模拟的嵌套事务在锁的持有方便还是与嵌套事务有所区别。当通过保存点即使来模拟事务时,用户无法选择哪些需要被子事务几次,哪些需要被父事务保留。这就是说,无论有多少个保存点,所有被锁住的对象都可以被得到和访问。而在嵌套事务中,不同子事务在数据库对象上持有的锁是不同的。

然而,如果系统支持在嵌套事务中并行的执行各个子事务,在这种情况下,采用保存点的扁平事务来模拟嵌套事务就不切实际了。这从另一个方面反映出,想要实现事务间的并行性,需要真正的支持嵌套事务。

分布式事务通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。例如跨行转账

InnoDB支持扁平事务,带有保存点的事务,链事务,分布式事务。对于嵌套事务,其原生不支持,因此,对有并行事务需求的用户来说,MySQL数据库或InonoDB引擎就显得无能为力了。

事务的实现

事务的隔离性由锁来实现。原子性,一致性,持久性通过数据库的redo log和undo log来完成,redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性

redo和undo的作用都可以视为一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的页的物理修改操作。undo是逻辑日志,根据每行记录进行记录

redo

基本概念

重做日志用来实现事务的持久性,即事务ACID中的D。其由两部分组成 : 一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的.

InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交时(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,事务的COMMIT操作才算完成。这里的日志指的是重做日志,在InnoDB中,由两部分组成,即redo log和undo log。redo log用来保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能。redo log基本上是需要进行随机读写的。

为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB都需要调用一个fsync操作。由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能能决定事务提交的性能,也就是数据库的性能。

InnoDB允许用户手动设置成非持久性,以此提高数据库的性能,即当事务提交时,日志不写入重做日志文件,而是等待一个时间周期后再执行fsync操作。由于并非强制在事务提交时进行一次fsync操作,显然这可以显著提高数据的性能。但是当数据库宕机时,由于部分日志未刷新到磁盘,因此会丢失最后一段时间的事务。

参数innodb_flush_log_at_trx_commit可以用来控制重做日志刷新到磁盘的策略。具体的可以百度。

在MySQL数据库中海油一种二进制日志(binlog),其用来进行POINT-TIME(PIT)的回复及主从复制(Relication)环境的简历。从表面上看其和重做日志非常相似,都是记录了对于数据库的操作的日志。然而,从本质上来说,两者有着非常大的不同。

首先,重做日志是InnoDB存储引擎层产生的,而二进制日志是在MySQL数据库的上层产生的,并且二进制日志不仅针对于InnoDB存储引擎,MySQL数据库中的任何存储引擎都会产生二进制日志。

其次,两种日志记录的内容形式不同。MySQL数据库上层的二进制日志是一种逻辑日志,其记录对应的SQL语句。而InnoDB的重做日志是物理格式日志,其记录的是对每个页的修改。

此外,两种日志记录写入磁盘的时间不同,二进制日志只在事务提交完成后进行一次写入。而InnoDB的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的。

二进制日志仅在事务提交时记录,并且对于每一个事务,仅包含对应事务的一个日志。而对于InnoDB的重做日志,由于其记录的是物理操作日志,因此每个事务对于多个日志条目,并且事务的重做日志是并发写入的,并非在事务提交时写入的,故其在文件中记录的顺序并非是事务开始的顺序。

log block

在InnoDB中,重做日志都是以512字节进行存储的。这意味着重做日志缓存,重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节。

若一个页中产生的日志数量大于512字节,那么需要分隔为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要doublewrite技术。

...略...

undo

基本概念

重做日志记录了事务的行为,可以很好地通过其对页进行重做操作。但是数据有时还需要回滚,这时就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户只需的事务或语句由于某种原因失败了,又或者用户用一条rollback语句请求回滚,这样就可以利用这些undo信息将数据回滚到修改之前的样子。

redo存档在重做日志文件中,与redo不同,undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)。undo段位于共享表空间内。

用户通常对undo有这样的误解 : undo用于将数据库物理地恢复到执行语句或事务之前的样子,但事实并非如此。undo是逻辑日志,因此只是将数据库的逻辑的回复到原来的样子。所有的修改都没逻辑的取消了,但是数据结构和页本身在回滚后可能大不相同。这是因为在多用户并发的系统中,可能会有数十上百甚至上千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几个记录,同时还有别的事务在对同一个页中的 另几条记录进行修改。因此不能将一个页回滚到事务开始时的样子,因为这会影响到其他事务正在进行的工作。

例如,用户执行了一个insert 10w条记录的事务,这个事务会导致分配一个新的段,即表空间增大。在用户执行rollback时,会将插入的事务进行回滚,但是表空间的大小并不会因此缩减。因此,当InnoDB存储引擎回滚时,它实际上做的是与之前相反的操作。对于每个insert操作,都会去完成一个delete;对于每个delete,都会完成一个insert;对于每个update,都对完成一个相反的update。

除了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来实现的。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取到之前的行版本信息,以此实现非锁定读。

最后很重要的一点,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。

... 略 ...

事务控制语句

在MySQL的命令行的默认设置下,事务都是自动提交(auto commit)的,即执行SQL语句就会马上执行COMMIT操作。因此要显式地开启一个事务需使用命令BEGIN,START TRANSACTION,或者执行命令SET AUTOCOMMIT=0,禁用当前会话的自动提交。注意,每个数据库厂商自动提交的设置都不相同。下面看看有哪些事务控制语句

  • START TRANSACTION : 显式的开启一个事务
  • COMMIT : 这个语句的最简形式就是COMMIT。也可以详细一些,用COMMIT WORK,不过二者几乎是等价的。COMMIT会提交事务,并使已对数据做的所有修改称为永久性的。
  • ROLLBACK : 这个语句的最简形式就是ROLLBACK。也可以详细一些,用ROLLBACK,不过二者几乎是等价的。回滚并结束用户的事务,并撤销事务中所有未提交的修改。
  • SAVEPOINT identifier : 在事务中创建一个保存点,一个事务可以有多个SAVEPOINT.
  • RELEASE SAVEPOINT identifier : 删除一个事务的保存点。
  • ROLLBACK TO [SAVEPOINT] identifier : 这个语句和SAVEPOINT命令一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何操作
  • SET TRANSACTION : 用来设置事务的隔离级别。InnoDB提供的事务级别有 : READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE。

START TRANSACTION,BEGIN语句都可以在命令行下显式的开启一个事务。但是在存储过程中,MySQL数据库的分析器会将BEGIN识别成BEGIN...END,因此在存储过程中只能使用START TRANSACTION语句开启一个事务。

COMMIT和COMMIT WORK语句基本是一致的,都是用来提交事务的。不同之处在于COMMIT WORK用来控制事务结束后的行为是CHAIN还是RELEASE的,如果是CHAIN方式,那就变成了链事务。

可以通过参数completion_type来进行控制,该参数默认为0,标识没有任何操作,在这种设置下COMMIT和COMMIT WORK是完全等价的。当参数为1时,COMMIT WORK等同于COMMIT AND CHAIN,表示马上自动开启一个相同隔离级别的事务。如下

create table t(`id` int);
set @@completion_type=1;
start transaction;
insert into t value (1);
commit work;
insert into t value (2);
rollback;
select * from t;
# 只能查询出id为1的一条记录

当completion_type为2时,COMMIT WORK等同于COMMIT AND RELEASE。在事务提交后会自动断开与服务器的连接。

ROLLBACK和ROLLBACK WORK与COMMIT和COMMIT WORK的用法一致。

SAVEPOINT记录了一个保存点,可以通过ROLLBACK TO SAVEPOINT来回滚到某个保存点,但是如果回滚到一个不存在的保存点,则会抛出异常

InnoDB存储引擎的中的事务都是原子性的,这说明下述两种情况 : 构成事务的每条语句都会提交(称为永久)或者全部回滚。这种保护还延伸到每个语句。一条语句要么完全成功,要么完全回滚(注意,这里是针对单条语句的回滚)。因此一条语句执行失败并抛出异常时,并不会导致先前事务中已经执行的语句回滚。所有的执行都会被保留,必须由用户自己来决定是否对其进行提交或回滚操作。如下:

drop table if exists `t`;
create table t(`id` int,primary key(`id`));
start transaction;
insert into t value (1);
insert into t value (1);
# 抛出主键不能重复的异常
select * from t;
# 可以查询出id为1的一条记录

另一个容易犯的错误是ROLLBACK TO SAVEPOINT,虽然有ROLLBACK,但其并不是真正的结束一个事务,因此即使只需了ROLLBACK TO SAVEPOINT,之后也需要显式的执行COMMIT或ROLLBACK命令。如下 :

drop table if exists `t`;
create table t(`id` int,primary key(`id`));
start transaction;
insert into t value (1);
savepoint t1;
insert into t value (2);
savepoint t2;
release savepoint t1;
insert into t value (2);
# 抛出主键重复的异常
rollback to savepoint t2;
select * from t;
# 依然可以查询到两条记录
rollback;
# 全部回滚
select * from t;
# 查询不到记录了

隐式提交的SQL语句

以下SQL语句会产生一个隐式的提交操作,就是不能进行回滚

  • DDL语句: ALTER DATABASEUPGRADE DATA DIRECTORY NAME,ALTER EVENT, ALTER PROCEDURE, ALTER TABLE, ALTER VIEW,CREATE DATABASE, CREATE EVENT, CREATE INDEX, CREATE PROCEDURE, CREATE TABLE, CREATE TRIGGER, CREATE VIEW,DROP DATABASE, DROP EVENT, DROP INDEX, DROP PROCEDURE,DROP TABLE, DROP TRIGGER, DROP VIEW, RENAME TABLE,TRUNCATE TABLE。
  • 用来隐式地修改 MySQL架构的操作: CREATE USER、 DROP USER、 GRANT 、RENAME USER、 REVOKE、 SET PASSWORD。
  • 管理语句: ANALYZE TABLE、 CACHE INDEX、 CHECK TABLE、 LOAD INDEX INTO CACHE、 OPTIMIZE TABLE、 REPAIR TABLE。

InnoDB的应用需要在考虑每秒请求数(Question Per Second,QPS)的同事,应该关注每秒事务的处理能力(Transaction Per Second, TPS)。

计算TPS的方法(com_commit+com_collback)/time。但是利用这种方法进行计算的前提是 : 所有事务必须是显式提交的,隐式存在的提交和回滚(默认autocommit=1),不会计算到这两个变量中。

事务的隔离级别

...略...

分布式事务

MySQL数据库的分布式事务

InnoDB提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。分布式事务是指允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高。在使用分布式事务时,InnoDB存储引擎的事务隔离级别必须设置为SERIALIZABLE。

XA事务允许不同数据库之前的分布式事务,如一台服务器是MySQL数据库,另一台是Oracle(或者其他的),只要参与在全局事务中的每个节点都支持XA事务。

内部XA事务

之前说的分布式事务是外部事务,即资源管理器是MySQL数据库本身。在MySQL数据库中还存在另外一种分布式事务,其在存储引擎与插件之前,又或者在存储引擎与存储引擎之间,称之为内部XA事务。

最常见的内部XA事务存在于binlog与InnoDB存储引擎之间。
...略...

不好的事务习惯

在循环中提交

提交事务时会写一次重做日志,如果是循环一千次,那么就会写一千次重做日志。当是整个循环结束之后再提交时,那么就只会写一次。

使用自动提交

...略...

使用自动回滚

...略...

长事务

顾名思义就是执行时间长的事务。有的事务执行可能需要非常长的时间。可能1个小时,可能4,5个小时,这取决于数据硬件的配置。DBA和开发人员本身能做的事情非常少。然而,由于ACID的特性,这个操作被封装在一个事务中完成。这就产生了一个问题,在执行过程中,当数据库或操作系统,硬件发生问题时,重新开始事务变得不可接受。数据库需要回滚所有已经发生的变化,而这个过程可能比产生这些变化的时间还长。因此对于长事务的问题,有时可以转化为小批量的事务进行处理。当发生错误时,只需要从发生错误的位置继续执行就行。

linux编程学习 05-02 网络编程

上一篇:http://jinblog.com/archives/866.html

网络高级编程

  • 前面介绍的函数如recv,send,read和write等函数都是阻塞性函数,若资源没有准备好,则调用该函数的进程将进入阻塞状态,下面介绍两种I/O复用的解决方案

    • fcntl函数实现(非阻塞)
    • select函数

I/O多路转换-select函数

int select(int maxfdp1,fd_set readfds,fd_set writefds,fd_set exceptfds,struct timeval timeout);

  • 头文件
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
  • 返回准备就绪的描述符数,若超时则为0,若出错则为-1
  • timval结构体
struct timeval
{
        long tv_sec;//seconds
        long tv_usec;//and microseconds
};
  • 参数

    • maxfdp1:最大fd加1(max fd plus 1),在三个描述符集中找出最高描述符编号值,然后加1,就是第一个参数的值
    • readfds,writefds,exceptfds:是指向描述符集的指针。这三个描述符集说明了我们关小的可读,可写或处于异常条件的各个描述符。每个描述符集存放在一个fd_set数据类型中
    • timeout:指定愿意等待的时间

      • NULL:永远等待,知道捕捉到信号或文件描述符已准备好为止
      • 具体值:struct timeval类型的指针,若等待为timeout时间,还没有文件描述符准备好,就立即返回
      • 0:从不等待,测试所有指定的描述符立即返回
  • 传向select的参数告诉内核

    • 我们所关心的描述符
    • 对于每个描述符我们所关心的条件(是否可读一个给定的描述符,是否可写一个指定的描述符,是否关心一个描述符的异常条件)
    • 希望等待多长时间(可以永远等待,等待一个固定量的时间,或完全不等待)
  • 从select返回时内核告诉我们

    • 已准备好的描述符的数量
    • 哪一个描述符已准备好读,写或异常条件
    • 使用这种返回值,就可调用相应的I/O函数(一般是write或read),并且确知该函数不会阻塞
  • select函数根据希望进行的文件操作对文件描述符进行分类处理,这里,对文件描述符的处理主要设计4个宏函数

    • FD_ZERO(fd_set* set) 清除一个文件描述符集
    • FD_SET(int fd,fd_set* set) 将一个文件描述符加入文件描述符集中
    • FD_CLR(int fd,fd_set* set) 将一个文件描述符从文件描述符集中清除
    • FD_ISSET(int fd,fd_set* set) 测试该集中的一个给定位是否有变化
  • 在使用select函数之前,首先使用FD_ZERO和FD_SET来初始化文件描述符集,并使用select函数时,可循环使用FD_ISSET测试描述符集,在执行完成对相关的文件描述符后,可用FD_CLR来清除描述符集

fcntl函数实现示例

#include <stdio.h>
// atoi函数
#include <stdlib.h>
// wait函数
#include <sys/wait.h>
// I/O函数
#include <unistd.h>
#include <time.h>
// socket 系列函数
#include <sys/socket.h>
// sockaddr_in 结构体
#include <netinet/in.h>
// hton 函数
#include <arpa/inet.h>
#include <signal.h>
// memset函数
#include <string.h>
// pthread库
#include <pthread.h>
// errno
#include <errno.h>
// fcntl函数
#include <fcntl.h>
// ctrl + c时关掉服务器socket
void siginit_handle(int);
// 初始化指定数目的线程处理连接
void child_thread_init(int);
// 客户端链表节点结构体
struct client_fd
{
        int fd;
        struct client_fd* pre;
        struct client_fd* next;
};
typedef struct client_fd client_fd;
// 链表
client_fd* client_fd_link = NULL;
// 链表互斥锁
pthread_mutex_t link_mutex;
// 服务器socket
int server_fd = 0;
// 添加链表元素
int add_client(int);
// 删除链表元素,通过fd删除元素
int del_client_by_fd(int);
// 通过节点地址删除元素
int del_client_by_client(client_fd*);
// 服务器连接日志
void server_log(struct sockaddr_in*);
// ctrl + c关闭服务器socket
void sigint_handle(int sig);

int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incorrect parameter\n");
                return 1;
        }
        if(SIG_ERR == signal(SIGINT,sigint_handle))
        {
                perror("signal error");
                return 1;
        }
        // 初始化互斥锁
        pthread_mutex_init(&link_mutex,NULL);
        struct sockaddr_in server_attr;
        memset(&server_attr,0,sizeof(server_attr));
        /*
         * 1. 创建socket
         * socket创建在内核中,是一个结构体
         * AF_INET : IPV4
         * SOCK_STREAM : TCP协议
         */
        server_fd = socket(AF_INET,SOCK_STREAM,0);
        if(server_fd == -1)
        {
                perror("sock error");
                return 1;
        }

        /*
         * 2. 设置SO_REUSEADDR,避免重启导致bind报错
         */
        int opt = 1;
        setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(int));
        /*
         * 3. 调用bind函数将socket与地址(ip,port)进行绑定
         */
        server_attr.sin_family = AF_INET;
        // 转换位网络字节序
        server_attr.sin_port = htons((short)atoi(argv[1]));
        // 表示所有ip,也可以指定第一个ip,指定ip时要把主机字节序转换成网络字节序
        server_attr.sin_addr.s_addr = INADDR_ANY;
        // 绑定ip端口
        if(-1 == bind(server_fd,(struct sockaddr*)&server_attr,sizeof(server_attr)))
        {
                perror("bind error");
                return 1;
        }
        /*
         * 4. 调用listen函数启动监听,通知系统接收来自客户端的请求
         * 第二个参数代表客户端队列的长度
         * 执行成功后能用netstat查看
         */
        if(-1 == listen(server_fd,8))
        {
                perror("listen error");
                return 1;
        }

        // 忽略SIGPIPE信号处理函数,避免由于客户端断开连接,并由于read发送SIGPIPE信号导致进程结>束
        signal(SIGPIPE,SIG_IGN);
        int client_fd = 0;
        struct sockaddr_in client_attr;
        socklen_t addrlen = sizeof(client_attr);
        int client_flag = 0;
        // 开启子线程处理连接·
        child_thread_init(8);
        while(1)
        {
                /*
                * 5. 调用accept获得客户端连接,并返回一个新的socket文件描述符,新的文件描述符放>进client_fd链表中
                * 没有客户端连接,此函数会阻塞,直到获得一个客户端连接
                */
                client_fd = accept(server_fd,(struct sockaddr*)&client_attr,&addrlen);
                if(client_fd <= 0)
                {
                        continue;
                }
                server_log(&client_attr);
                // 修改socket为非阻塞读写
                client_flag = fcntl(client_fd,F_GETFL);
                client_flag |= O_NONBLOCK;
                fcntl(client_fd,F_SETFL,client_flag);
                add_client(client_fd);
                // 线程是共享的进程资源,这里不要关闭,不然线程里面的也没了
                // close(client_fd);
        }
        // close(server_fd);
        return 0;
}
void sigint_handle(int sig)
{
        close(server_fd);
        exit(0);
}
void server_log(struct sockaddr_in* client_attr)
{
        char ip[16] = {'\0'};
        // 结构体的的数据不能直接显示,需要先转换成本机字节序
        inet_ntop(AF_INET,&client_attr->sin_addr.s_addr,ip,sizeof(ip));
        printf("connected by %s:%d\n",ip,ntohs(client_attr->sin_port));
}
void* thread_handle(void* data)
{
        /*
        * 5. 遍历客户端socket_fd,对有输入的客户端进行回应
        */
        client_fd* temp;
        char res[512] = {'\0'},buf[512] = {'\0'};
        int read_len = 0,res_len = sizeof(res);
        while(1)
        {
                temp = client_fd_link;
                while(temp != NULL)
                {
                        read_len = read(temp->fd,res,res_len);
                        // 客户端关闭,则关闭客户端连接
                        if(read_len == 0)
                        {
                                perror("read");
                                del_client_by_client(temp);
                                break;
                        }
                        else if(read_len > 0)
                        {
                                // buf 与 res不能一样
                                sprintf(buf,"thread : %lu\nmessage : %s\n",pthread_self(),res);

                                sprintf(res,"%s\n",buf);

                                if(write(temp->fd,res,res_len) != res_len)
                                {
                                        // 客户端关闭,则关闭客户端连接
                                        if(errno == EPIPE)
                                        {
                                                perror("write");
                                                del_client_by_client(temp);
                                                break;
                                        }
                                }
                        }
                        temp = temp->next;
                }
        }
        return (void*)NULL;
}
void child_thread_init(int max)
{
        pthread_t tid = 0;
        // 初始化线程属性,用来创建分离的线程,使子线程结束后自动回收
        pthread_attr_t thread_attr;
        pthread_attr_init(&thread_attr);
        pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
        // 子线程处理I/O,达到并发的效果
        while(max > 0)
        {
                pthread_create(&tid,&thread_attr,thread_handle,(void*)NULL);
                max--;
        }
}
// 添加链表元素
int add_client(int fd)
{
        pthread_mutex_lock(&link_mutex);
        if(client_fd_link == NULL)
        {
                client_fd_link = malloc(sizeof(client_fd));
                client_fd_link->pre = NULL;
                client_fd_link->next = NULL;
                client_fd_link->fd = fd;
                pthread_mutex_unlock(&link_mutex);
                return 0;
        }
        client_fd* temp = client_fd_link;
        while(temp->next != NULL)
        {
                temp = temp->next;
        }
        temp->next = malloc(sizeof(client_fd));
        temp->next->pre = temp;
        temp->next->next = NULL;
        temp->next->fd = fd;
        pthread_mutex_unlock(&link_mutex);
        return 0;
}
// 删除链表元素,通过fd删除元素
int del_client_by_fd(int fd)
{
        pthread_mutex_lock(&link_mutex);
        if(client_fd_link == NULL)
        {
                pthread_mutex_unlock(&link_mutex);
                return -1;
        }
        client_fd* temp = client_fd_link->next;
        // 链表第一个元素比较特殊,做特殊处理
        if(fd == client_fd_link->fd)
        {
                free(client_fd_link);
                client_fd_link = temp;
                if(client_fd_link != NULL)
                {
                        client_fd_link->pre = NULL;
                }
                pthread_mutex_unlock(&link_mutex);
                return 0;
        }
        while(temp != NULL)
        {
                if(temp->fd == fd)
                {
                        temp->pre->next = temp->next;
                        if(temp->next != NULL)
                        {
                                temp->next->pre = temp->pre;
                        }
                        close(temp->fd);
                        free(temp);
                        pthread_mutex_unlock(&link_mutex);
                        return 0;
                }
                temp = temp->next;
        }
        pthread_mutex_unlock(&link_mutex);
        return -1;
}
// 通过节点地址删除元素
int del_client_by_client(client_fd* client)
{
        pthread_mutex_lock(&link_mutex);
        if(client_fd_link == NULL)
        {
                pthread_mutex_unlock(&link_mutex);
                return -1;
        }
        client_fd* temp = client_fd_link->next;
        // 链表第一个元素比较特殊,做特殊处理
        if(client == client_fd_link)
        {
                free(client_fd_link);
                client_fd_link = temp;
                if(client_fd_link != NULL)
                {
                        client_fd_link->pre = NULL;
                }
                pthread_mutex_unlock(&link_mutex);
                return 0;
        }
        while(temp != NULL)
        {
                if(temp == client)
                {
                        temp->pre->next = temp->next;
                        if(temp->next != NULL)
                        {
                                temp->next->pre = temp->pre;
                        }
                        close(temp->fd);
                        free(temp);
                        pthread_mutex_unlock(&link_mutex);
                        return 0;
                }
                temp = temp->next;
        }
        pthread_mutex_unlock(&link_mutex);
        return -1;
}

select函数实现示例

#include <stdio.h>
// atoi函数
#include <stdlib.h>
// wait函数
#include <sys/wait.h>
// I/O函数
#include <unistd.h>
#include <time.h>
// socket 系列函数
#include <sys/socket.h>
// sockaddr_in 结构体
#include <netinet/in.h>
// hton 函数
#include <arpa/inet.h>
#include <signal.h>
// memset函数
#include <string.h>
// pthread库
#include <pthread.h>
// errno
#include <errno.h>
// fcntl函数
#include <fcntl.h>
#include <sys/types.h>
#include <sys/time.h>
// ctrl + c时关掉服务器socket
void siginit_handle(int);
// 初始化指定数目的线程处理连接
void child_thread_init(int);
// 客户端链表节点结构体
struct client_fd
{
        int fd;
        struct client_fd* pre;
        struct client_fd* next;
};
typedef struct client_fd client_fd;
// 链表
client_fd* client_fd_link = NULL;
// 链表互斥锁
pthread_mutex_t link_mutex;
// 服务器socket
int server_fd = 0;
// 添加链表元素
int add_client(int);
// 删除链表元素,通过fd删除元素
int del_client_by_fd(int);
// 通过节点地址删除元素
int del_client_by_client(client_fd*);
// 服务器连接日志
void server_log(struct sockaddr_in*);
// ctrl + c关闭服务器socket
void sigint_handle(int sig);
// 把链表中的fd加入到set中,并返回socket描述符最大的一个
int init_set(fd_set*);
int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incorrect parameter\n");
                return 1;
        }
        if(SIG_ERR == signal(SIGINT,sigint_handle))
        {
                perror("signal error");
                return 1;
        }
        // 初始化互斥锁
        pthread_mutex_init(&link_mutex,NULL);
        struct sockaddr_in server_attr;
        memset(&server_attr,0,sizeof(server_attr));
        /*
         * 1. 创建socket
         * socket创建在内核中,是一个结构体
         * AF_INET : IPV4
         * SOCK_STREAM : TCP协议
         */
        server_fd = socket(AF_INET,SOCK_STREAM,0);
        if(server_fd == -1)
        {
                perror("sock error");
                return 1;
        }

        /*
         * 2. 设置SO_REUSEADDR,避免重启导致bind报错
         */
        int opt = 1;
        setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(int));
        /*
         * 3. 调用bind函数将socket与地址(ip,port)进行绑定
         */
        server_attr.sin_family = AF_INET;
        // 转换位网络字节序
        server_attr.sin_port = htons((short)atoi(argv[1]));
        // 表示所有ip,也可以指定第一个ip,指定ip时要把主机字节序转换成网络字节序
        server_attr.sin_addr.s_addr = INADDR_ANY;
        // 绑定ip端口
        if(-1 == bind(server_fd,(struct sockaddr*)&server_attr,sizeof(server_attr)))
        {
                perror("bind error");
                return 1;
        }
        /*
         * 4. 调用listen函数启动监听,通知系统接收来自客户端的请求
         * 第二个参数代表客户端队列的长度
         * 执行成功后能用netstat查看
         */
        if(-1 == listen(server_fd,8))
        {
                perror("listen error");
                return 1;
        }

        // 忽略SIGPIPE信号处理函数,避免由于客户端断开连接,并由于read发送SIGPIPE信号导致进程结>束
        signal(SIGPIPE,SIG_IGN);
        int client_fd = 0;
        struct sockaddr_in client_attr;
        socklen_t addrlen = sizeof(client_attr);
        // 开启子线程处理连接·
        child_thread_init(8);
        while(1)
        {
                /*
                * 5. 调用accept获得客户端连接,并返回一个新的socket文件描述符,新的文件描述符放>进client_fd链表中
                * 没有客户端连接,此函数会阻塞,直到获得一个客户端连接
                */
                client_fd = accept(server_fd,(struct sockaddr*)&client_attr,&addrlen);
                if(client_fd <= 0)
                {
                        continue;
                }
                server_log(&client_attr);
                add_client(client_fd);
                // 线程是共享的进程资源,这里不要关闭,不然线程里面的也没了
        }
        return 0;
}
void sigint_handle(int sig)
{
        close(server_fd);
        exit(0);
}
void server_log(struct sockaddr_in* client_attr)
{
        char ip[16] = {'\0'};
        // 结构体的的数据不能直接显示,需要先转换成本机字节序
        inet_ntop(AF_INET,&client_attr->sin_addr.s_addr,ip,sizeof(ip));
        printf("connected by %s:%d\n",ip,ntohs(client_attr->sin_port));
}
void response(client_fd* temp)
{
        char res[512] = {'\0'},buf[512] = {'\0'};
        int read_len = 0,res_len = sizeof(res);
        read_len = read(temp->fd,res,res_len);
        // 客户端关闭,则关闭客户端连接
        if(read_len == 0)
        {
                printf("close connection\n");
                del_client_by_client(temp);
                return;
        }
        else if(read_len > 0)
        {
                // buf 与 res不能一样
                sprintf(buf,"thread : %lu\nmessage : %s\n",pthread_self(),res);

                sprintf(res,"%s\n",buf);

                if(write(temp->fd,res,res_len) != res_len)
                {
                        // 客户端关闭,则关闭客户端连接
                        if(errno == EPIPE)
                        {
                                printf("close connection\n");
                                del_client_by_client(temp);
                                return;
                        }
                }
        }
}
void* thread_handle(void* data)
{
        /*
        * 5. 遍历客户端socket_fd,对有输入的客户端进行回应
        */
        client_fd* temp;
        fd_set set;
        int max = init_set(&set);
        struct timeval time = {2,0};
        int n = 0;
        while(1)
        {
                // printf("n : %d;max : %d\n",n,max);
                // 注意:是文件描述符最大的加1,
                n = select(max + 1,&set,NULL,NULL,&time);
                temp = client_fd_link;
                while(temp != NULL && n > 0)
                {
                        if(FD_ISSET(temp->fd,&set) == 0)
                        {
                                continue;
                        }
                        response(temp);
                        temp = temp->next;
                        n--;
                }
                // 时间需要重新设置
                time.tv_sec = 2;
                time.tv_usec = 0;
                // 用最新的连接信息设置set,重设最大值
                max = init_set(&set);
        }
        return (void*)NULL;
}
void child_thread_init(int max)
{
        pthread_t tid = 0;
        // 初始化线程属性,用来创建分离的线程,使子线程结束后自动回收
        pthread_attr_t thread_attr;
        pthread_attr_init(&thread_attr);
        pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
        // 子线程处理I/O,达到并发的效果
        while(max > 0)
        {
                pthread_create(&tid,&thread_attr,thread_handle,(void*)NULL);
                max--;
        }
}
// 添加链表元素
int add_client(int fd)
{
        pthread_mutex_lock(&link_mutex);
        if(client_fd_link == NULL)
        {
                client_fd_link = malloc(sizeof(client_fd));
                client_fd_link->pre = NULL;
                client_fd_link->next = NULL;
                client_fd_link->fd = fd;
                pthread_mutex_unlock(&link_mutex);
                return 0;
        }
        client_fd* temp = client_fd_link;
        while(temp->next != NULL)
        {
                temp = temp->next;
        }
        temp->next = malloc(sizeof(client_fd));
        temp->next->pre = temp;
        temp->next->next = NULL;
        temp->next->fd = fd;
        pthread_mutex_unlock(&link_mutex);
        return 0;
}
// 删除链表元素,通过fd删除元素
int del_client_by_fd(int fd)
{
        pthread_mutex_lock(&link_mutex);
        if(client_fd_link == NULL)
        {
                pthread_mutex_unlock(&link_mutex);
                return -1;
        }
        client_fd* temp = client_fd_link->next;
        // 链表第一个元素比较特殊,做特殊处理
        if(fd == client_fd_link->fd)
        {
                free(client_fd_link);
                client_fd_link = temp;
                if(client_fd_link != NULL)
                {
                        client_fd_link->pre = NULL;
                }
                pthread_mutex_unlock(&link_mutex);
                return 0;
        }
        while(temp != NULL)
        {
                if(temp->fd == fd)
                {
                        temp->pre->next = temp->next;
                        if(temp->next != NULL)
                        {
                                temp->next->pre = temp->pre;
                        }
                        close(temp->fd);
                        free(temp);
                        pthread_mutex_unlock(&link_mutex);
                        return 0;
                }
                temp = temp->next;
        }
        pthread_mutex_unlock(&link_mutex);
        return -1;
}
// 通过节点地址删除元素
int del_client_by_client(client_fd* client)
{
        pthread_mutex_lock(&link_mutex);
        if(client_fd_link == NULL)
        {
                pthread_mutex_unlock(&link_mutex);
                return -1;
        }
        client_fd* temp = client_fd_link->next;
        // 链表第一个元素比较特殊,做特殊处理
        if(client == client_fd_link)
        {
                free(client_fd_link);
                client_fd_link = temp;
                if(client_fd_link != NULL)
                {
                        client_fd_link->pre = NULL;
                }
                pthread_mutex_unlock(&link_mutex);
                return 0;
        }
        while(temp != NULL)
        {
                if(temp == client)
                {
                        temp->pre->next = temp->next;
                        if(temp->next != NULL)
                        {
                                temp->next->pre = temp->pre;
                        }
                        close(temp->fd);
                        free(temp);
                        pthread_mutex_unlock(&link_mutex);
                        return 0;
                }
                temp = temp->next;
        }
        pthread_mutex_unlock(&link_mutex);
        return -1;
}
int init_set(fd_set* set)
{
        client_fd* temp = client_fd_link;
        // 清空set
        FD_ZERO(set);
        int max = 0;
        while(temp != NULL)
        {
                // 获取最大的set
                if(max < temp->fd)
                {
                        max = temp->fd;
                }
                // 把client_fd加入set中
                FD_SET(temp->fd,set);
                temp = temp->next;
        }
        return max;
}

守护进程

  • 守护进程(deamon)是生存期长的一种进程。他们常常在系统装入时启动,在系统关闭时终止。
  • 所有守护进程都以超级用户(用户ID为0)的优先权运行
  • 守护进程没有控制终端
  • 守护进程的父进程都是init进程

守护进程编程步骤

  1. 使用umask将文件模式创建创建屏蔽字设置为0
  2. 调用fork,然后让父进程退出(exit)
  3. 调用setsid创建一个新会话
  4. 将当前工作目录更改为根目录
  5. 关闭不需要的文件描述符

守护进程出错处理

  • 由于守护进程完全脱离了控制终端,因此,不能像其他程序一样通过输出错误信息到控制台的方式来通知程序员
  • 通常的方式是使用syslog服务,将出错信息输入到“/var/log/syslog”系统日志文件中
  • syslog是linux中的系统日志管理服务,通过守护进程syslog来维护

syslog服务说明

  • openlog函数用于打开系统日志服务的一个连接
  • syslog函数用于向日志文件中写入消息,在这里可以规定消息的优先级,消息的输出格式等
  • closelog函数用于关闭系统日志服务的连接
openlog函数

void openlog(char* ident,int option,int facility);

  • 头文件
#include <syslog.h>
  • 参数

    • ident:要向每个消息加入的字符串,通常为程序名称
    • option

      • LOG_CONS 若日志消息不能通过发送至syslog,则将该消息写至控制台
      • LOG_NDELAY 立即打开linux域数据报套接口至syslog守护进程。通常,在记录第一条消息之前,该套接口不打开
      • LOG_PERROR 除将日志发送给syslog外,还将他写至stderr
      • LOG_PID 每条消息都包含进程id,此选择项可供对每个请求都fork一个子进程的守护进程使用
    • facility

      • LOG_AUTH 授权程序,如login,su,getty等
      • LOG_CRON cron和at
      • LOG_DAEMON 系统守护进程,如ftpd,routed等
      • LOG_KERN 内核产生的消息
      • LOG_LOCAL0~7 保留由本地使用
      • LOG_LPR 行打系统,如lpd,lpc等
      • LOG_MAIL 邮件系统
      • LOG_NEWSU senet网络新闻系统
      • LOG_SYSLOG syslog守护进程本身(用这个就ok)
      • LOG_USER 来自其他用户进程的消息
      • LOG_UUCP UUCP系统
syslog和closelog函数

void syslog(int priority,char* format,...);
void closelog(void);

  • 头文件
#include <syslog.h>
  • 参数

    • priority:消息优先级

      • LOG_EMERG 紧急(系统不可使用,最高优先级)
      • LOG_ALERT 必须立即修复的条件
      • LOG_CRIT 临界条件(例如,硬设备出错)
      • LOG_ERR 出错条件
      • LOG_WARNING 警告条件
      • LOG_NOTICE 正常,但重要的条件
      • LOG_INFO 信息性消息
      • LOG_DEBUG 调试排错消息(最低优先级)

示例

示例为select函数例子的main函数部分,注意要把头文件加上去和其他部分补齐才能运行

int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incorrect parameter\n");
                return 1;
        }
        if(SIG_ERR == signal(SIGINT,sigint_handle))
        {
                perror("signal error");
                return 1;
        }
        /* 守护进程变成步骤  */
        // 1. 使用umask将文件模式创建屏蔽字设置为0
        umask(0);
        // 2. 调用fork,然后让父进程退出
        int pid = fork();
        if(pid > 0)
        {
                exit(0);
        }
        // 3. 调用setsid创建一个新会话
        setsid();
        // 4. 将当前工作目录更改为根目录
        chroot("/");
        // 5. 关闭不需要的文件描述符
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);

        /* 使用syslog */
        // 1. 打开
        openlog("jin",LOG_PID,LOG_SYSLOG);
        // 2. 记录
        syslog(LOG_INFO,"jin deamon start");
        // 3. 关闭
        closelog();

        // 初始化互斥锁
        pthread_mutex_init(&link_mutex,NULL);
        struct sockaddr_in server_attr;
        memset(&server_attr,0,sizeof(server_attr));
        /*
         * 1. 创建socket
         * socket创建在内核中,是一个结构体
         * AF_INET : IPV4
         * SOCK_STREAM : TCP协议
         */
        server_fd = socket(AF_INET,SOCK_STREAM,0);
        if(server_fd == -1)
        {
                perror("sock error");
                return 1;
        }

        /*
         * 2. 设置SO_REUSEADDR,避免重启导致bind报错
         */
        int opt = 1;
        setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(int));
        /*
         * 3. 调用bind函数将socket与地址(ip,port)进行绑定
         */
        server_attr.sin_family = AF_INET;
        // 转换位网络字节序
        server_attr.sin_port = htons((short)atoi(argv[1]));
        // 表示所有ip,也可以指定第一个ip,指定ip时要把主机字节序转换成网络字节序
        server_attr.sin_addr.s_addr = INADDR_ANY;
        // 绑定ip端口
        if(-1 == bind(server_fd,(struct sockaddr*)&server_attr,sizeof(server_attr)))
        {
                perror("bind error");
                return 1;
        }
        /*
         * 4. 调用listen函数启动监听,通知系统接收来自客户端的请求
         * 第二个参数代表客户端队列的长度
         * 执行成功后能用netstat查看
         */
        if(-1 == listen(server_fd,8))
        {
                perror("listen error");
                return 1;
        }

        // 忽略SIGPIPE信号处理函数,避免由于客户端断开连接,并由于read发送SIGPIPE信号导致进程结>束
        signal(SIGPIPE,SIG_IGN);
        int client_fd = 0;
        struct sockaddr_in client_attr;
        socklen_t addrlen = sizeof(client_attr);
        // 开启子线程处理连接·
        child_thread_init(8);
        while(1)
        {
                /*
                * 5. 调用accept获得客户端连接,并返回一个新的socket文件描述符,新的文件描述符放>进client_fd链表中
                * 没有客户端连接,此函数会阻塞,直到获得一个客户端连接
                */
                client_fd = accept(server_fd,(struct sockaddr*)&client_attr,&addrlen);
                if(client_fd <= 0)
                {
                        continue;
                }
                server_log(&client_attr);
                add_client(client_fd);
                // 线程是共享的进程资源,这里不要关闭,不然线程里面的也没了
        }
        return 0;
}

linux编程学习 05-01 网络编程

计算机联网的目的

  • 使用远程资源
  • 共享信息,程序和数据
  • 分布处理

协议

  • 计算机网络中实现通信必须有一些约定,如对速率,传输代码,代码结构,传输控制步骤和出错控制步骤等约定,这些约定即被称为通信协议
  • 在两个节点之间要成功进行通信,两个节点之间必须使用相同的“语言”,这些被通信各方共同遵守的约定,语言,规则被称为协议
  • 在internet中,最为通用的网络协议是TCP/IP协议

TCP/IP协议族

  • TCP/IP实际上是一个一起工作的通信家族,为网际数据通信通路
  • TCP/IP协议族大体上分为三个部分

    • internet协议,如IP协议,对应网络层
    • 传输控制协议(TCP)和用户数据报文协议(UDP),对应传输层
    • 处于TCP和UDP之上的一组协议专门开发的应用程序。他们包括:远程登陆协议(TELNET),文件传送协议(FTP),域名服务(DNS)和简单的邮件传送程序(SMTP),超文本传输协议(HTTP)等,对应应用层

网络层协议

  • internet协议(IP)

    • 该协议被设计成互联网分组交换通信网,以形成一个网际通信环境。他负责在源主机和目的主机之间传输来自其较高层软件的称为数据报文的数据块,他在源和目的地之间提供非连接型传递服务
  • 网际控制报文协议(ICMP)

    • ICMP实际上不是IP层部分,但直接同IP层一起工作,报告网络上的某些出错情况。允许网际路由器传输出错信息或测试报文。
  • 地址识别协议(ARP)
    -ARP实际上不是网络层部分,他处于IP和数据链路层之间,他是在32位IP地址和48位局域网物理地址之间执行翻译的协议。mac地址与IP地址的转换

传输层协议

  • 传输控制协议(TCP)

    • 可靠的面向连接的传输层服务
    • 主要功能

      • 监听输入对话建立请求
      • 请求另一网络站点对话
      • 可靠的发送和接收数据
      • 适度的关闭对话
  • 用户数据报文协议(UDP)

    • UDP提供不可靠的非连接型传输层服务

      • 他允许在源和目的站点之间传送数据,而不必在传送数据之前建立对话
      • 不使用TCP使用端对端差错校验
      • 传输层功能全部发挥,而开销却比较低
      • 主要用于那些不要求TCP协议的非连接型应用程序。例如,名字服务,网络管理,视频点播和网络会议等

应用层协议

Internet协议(IP)

  • IP的主要目的是为数据输入/输出网络提供基本算法,为高层协议提供无连接的传送服务。这意味着在IP将数据递交给接收站点以前不在传输站点和接收站点之间建立对话(虚拟链路)。它只是封装和传递数据,但不向发送者和接收者报告包的状态,不处理所原道的故障。
  • IP协议主要有以下四个主要功能

    • 数据传送
    • 寻址
    • 路由选择
    • 数据报文的分段
  • IP协议不注意包内的数据类型,他所知道的一切是必须将称为IP帧头的控制协议加到高层协议(TCP或者UDP)所接收的数据上

IP地址

  • 在TCP/IP网络中,每个主机都有唯一的地址,他是通过IP协议来实现的。
  • IP协议要求在每次与TCP/IP网络建立连接时,每台主机都必须为这个连接分配一个唯一的32位地址,因为在这个32位IP地址,不但可以用来识别某一台主机,而且还隐含着网际间的路径信息
  • 主机是指网络上的一个节点,不能简单的理解为一台计算机,实际上IP地址是分配给计算机的网络适配器(网卡)的,一台计算机可以有多个网络适配器,就可以有多个IP地址,一个网络适配器就是一个节点
  • IP地址为32位地址,一般以4个字节表示。每个字节的数字又用十进制表示,即每个字节的数的范围是0~255,且每个数字之间用“.”隔开,例如:192.168.1.8,这种记录方法称为“点-分”十进制号发。IP地址结构如下
    |网络类型|网络ID|主机ID|

IP地址分类

  • Internet地址可分为5类
地址类型第一个字节的十进制数
A000-127
B128-191
C192-233
D224-239
E240-255
  • A,B,C三类有InterNIC(Internet网络信息中心)在全球范围内同一分配,D,E类为特殊地址

端口号

  • TCP/UDP协议使用16位整数存储端口号,所以每个主机拥有65535个端口
  • 一些端口被IANA分配给指定应用

    • 21:FTP
    • 23:Telnet
    • 80:HTTP
    • RFC 1700(大约有2000个保留端口)

传输控制协议(TCP)

  • TCP(传输控制协议Transmission Control Protocol)是重要的传输层协议,TCP提供一种面向连接的,可靠的字节流服务
  • TCP协议的目的是允许数据同网络上的另外站点进行可靠的交换。他能提供端口编号的译码,以识别主机的应用程序,而且完成数据的可靠传输
  • TCP协议具有严格的内装差错校验算法确保数据的完整性
  • TCP协议是面向字节的顺序协议,这意味着包内的每个字节被分配一个顺序编号,并分配给每包一个顺序编号

用户数据报文协议(UDP)

  • UDP(用户数据报协议User Datagram Protocol)也是TCP/IP的传输层协议,他是无连接的,不可靠的传输服务。当接收数据时他不向发送方提供确认信息,他不提供输入包的顺序,如果出现丢失包或重份包的情况,也不会向发送方发出差错报文

    • 他允许在源和目的地站点间传送数据,而不必在传送数据之间建立对话
    • 不使用TCP使用的端对端差错校验
    • 传输层功能全部发挥,而开销却比较低
  • 由于他执行功能时具有较低的开销,因此执行速度比TCP快。他多半用于不需要可靠传输的应用程序,例如网络视频点播和视频会议等

TCP和UDP协议区别

  • TCP以连接为基础,即两台电脑必须先建立连接,然后才能传输数据,事实上,发送和接收的电脑必须一直互相通讯和联系。
  • UDP是一个无连接服务,数据可以直接发送而不必在两台电脑之间建立一个网络连接。他和有连接的TCP相比,占用带宽少,但是无法确认数据是否真正到达了客户端,而客户端收到的数据也不知道是否还是原来的发送数据

网络层其他数据路由协议

  • 路由协议分析数据包的地址并且决定传输数据到目的电脑最佳路线。他们也可以把大的数据分成几个部分,并且在目的地再把他们组合起来。IP处理实际上传输数据

    • ICMP(网络控制信息协议Internet Control Message Protocol)处理IP状态信息,比如能影响路由决策的数据错误或改变
    • RIP(路由信息协议Routing Information Protocol)他是几个决定信息传输的最佳路由路线协议中的一个
    • OSPF(Open Shortest Path First)一个用来决定路由的协议
    • ARP(地址解析协议Address Resolution Protocol)确定网络上一台电脑的数字地址
    • DNS(域名系统Domain Name System)从机器的名字确定机器的数字地址
    • RARP(反向地址解析协议Reverse Address Resolution Protocol)确定网络上一台计算机的地址,和ARP正好相反

其他用户服务协议

  • BOOTP(启动协议Boot Protocol)由网络服务器上取得启动信息,然后将本地的网络计算机启动
  • FTP(文件传输协议File Transfer Protocol)通过国际互联网从一台计算机上传输一个或多个文件到另一台计算机
  • TELNET(远程登陆)允许一个远程登陆,使用者可以从网络上的一台计算机通过TELNET连线到另一台机器,就向使用者直接在本地操作一样
  • EGP(外部网关协议Exterior Gateway Protocol)为外部网络传输路由信息
  • GGP(网关到网关协议Gateway-to-Gateway Protocol)在网关和网关之间传输路由协议
  • IGP(内部网关协议Interior Gateway Protocol)在内部网络传输路由协议

socket(套接字)

  • socket(套接字)是一种通讯机制,他包含一整套的调用接口和数据结构定义,他给应用程序提供了使用如TCP/UDP等网络协议进行通讯的手段
  • linux中的网络编程通过socket接口实现,socket既是一种特殊的I/O,提供对应的文件描述符。一个完整的socket都有一个相关描述(协议,本地地址,本地端口,远程地址,远程端口),每一个socket有一个本地的唯一socket,由操作系统分配
  • 内核提供的实现网络通信的接口

创建socket

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

  • 返回:成功返回描述符,出错返回-1
  • 头文件
#include <sys/socket.h>
  • socket创建在内核中,若创建成功返回内核文件描述表的socket描述符
  • 参数

    • domain

      • AF_INET IPV4因特网域
      • AF_INET6 IPV6因特网域
      • AF_UNIX unix域
      • AF_UNSPEC 未指定
    • protocol

      • 通常为0,表示按给定的域和套接字类型选择默认协议
    • type

      • SOCK_STREAM

        • 流式的套接字可以提供可靠的,面向连接的通讯流。他使用了TCP协议。TCP保证了数据传输的正确性和顺序性
      • SOCK_DGRAM

        • 数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。使用数据报协议UDP协议
      • SOCK_RAW

        • 原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等
      • SOCK_SEQPACKET

        • 长度固定,有序,可靠的面向链接报文传递

字节序

  • 不同体系结构的主机使用不同字节序存储器中保存多字节整数。字节存储顺序不同,有的系统是高位在前,低位在后,有的系统是低位在前,高位在后
  • 字节序分为大端和小端字节序
  • 网络协议使用网络字节序即大端字节序

字节序转换函数

  • 网络传输的数据大家是一定要同一顺序的,所以对于内部字节表示顺序和网络字节顺序不同的机器,就一定要对数据进行转换

    • uint32_t htonl(uint32_t hostlong);

      • 将一个32位整数由主机字节序转换成网络字节序
    • uint12_t htons(uint16_t hostshort);

      • 将一个16位整数由主机字节序转换成网络字节序
    • uint32_t ntohl(uint32_t netlong);

      • 将一个32位整数由网络字节序转换成主机字节序
    • uint16_t ntohs(uint16_t netshort);

      • 将一个16位整数由网络字节序转换成主机字节序

通用地址结构

#include <sys/socket.h>
struct sockaddr
{
        unsigned short sa_family;// Internet 地址族 AF_XXX
        char sa_data[14];// 14bytes的协议地址
};
  • sa_data包含了一些远程电脑的地址,端口和套接字的数目,他里面的数据是杂溶在一起的
  • sa_family一般来说,IPV4使用AF_INET
  • 在传递给需要地址结构的函数时,把指向该结构体的指针转换成(struct sockaddr*)传递进去

因特网地址结构

#include <netinet/in.h>
struct in_addr
{
        in_addr_t s_addr;// ipv4地址
};
struct sockaddr_in
{
        short int sin_family;// internet地址族如AF_INET(主机字节序)
        unsigned short int sin_port;// 端口号,16位值(网络字节序)
        struct in_addr sin_addr;// Internet地址,32位ipv4地址(网络字节序)
        unsigned char sin_zero[8];// 添0(为了格式对齐的填充位)
};
  • 这两个(sockaddr和sockaddr_in)数据类型是等效的,可以相互转换,通常使用sockaddr_in更为方便

IPV4地址族和字符地址间的转换

const char* inet_ntop(int domain,const void*restrict addr,char*restrict str,socklen_t size);

  • 返回:成功返回地址字符串指针,出错返回NULL
  • 功能:网络字节序转换成点分十进制
    int inet_pton(int domain,const char*restrict str,void*restrict addr);
  • 返回:成功返回1,无效格式返回0,出错返回-1
  • 功能:点分十进制转换成网络字节序
  • 头文件
#include <arpa/inet.h>
  • 参数

    • domain:internet地址族地址,如AF_INET
    • addr:internet地址,32位IPV4地址(网络字节序)
    • str:地址字符串(点分十进制)指针
    • size:地址字符串大小

填写IPV4地址族结构案例

struct sockaddr_in sin;// 定义一个sockaddr_in结构体
char buf[16];
memset(&sin,0,sizeof(sin));// 内存清0
sin.sin_family = AF_INET;// 填写internet地址族
sin.sin_port = hton((short)3001);// 填写端口号,转换成网络字节序
// 填充sin_addr,需要吧字符串型的ip地址转换成网络字节序
if(inet_pton(AF_INET,"192.168.2.1",&sin.sin_addr.s_addr) <= 0)
{
        // 错误处理
}
// 网络字节序不能直接输出,需要从网络字节序转换回字符串型才能输出
printf("%s\n",inet_ntop(AF_INET,&sin.sin_addr.s_addr,buf,sizeof(buf)));

TCP客户端服务器编程模型

  • 客户端调用序列

    • 调用socket函数创建套接字
    • 调用connect连接服务器端
    • 调用I/O函数(read/write)与服务器端通讯
    • 调用close关闭套接字
  • 服务器端调用序列

    • 调用socket函数创建套接字
    • 调用bind绑定本地地址和端口
    • 调用listen启动监听
    • 调用accept从已连接队列中提取客户连接,没有客户连接会阻塞
    • 调用I/O函数(read/write)与客户端通讯
    • 调用close关闭套接字

套接字与地址绑定

绑定地址

int bind(int sockfd,const struct sockaddr* addr,socklen_t len);

  • 返回:成功返回0,出错返回-1
  • 头文件
#include <sys/socket.h>
查找绑定到套接字的地址

int getsockname(inf sockfd,struct sockaddr*restrict addr,socklen_t*restrict alenp);

  • 返回:成功返回0,出错返回-1
  • 头文件
#include <sys/socket.h>
获取对方地址

int getpeername(int sockfd,struct sockaddr*restrict addr,socklen_t*restrict alenp);

  • 返回:成功返回0,出错返回-1
  • 头文件
#include <sys/socket.h>

建立连接

服务器端

int listen(int sockfd,int backlog);

  • 返回:成功返回0,出错返回-1。backlog指定进行客户端连接排队的队列长度
  • 头文件
#include <sys/socket.h>

int accept(int sockfd,struct sockaddr*restrict addr,socklen_t*restrict len);

客户端

int connect(int sockfd,const struct sockaddr* addr,socklen_t len);

  • 返回:成功返回0,出错返回-1
  • 头文件
#include <sys/socket.h>

特殊bind地址

  • 一台主机可以有多个网络接口和多个IP地址,如果我们只关心某个地址的连接请求,我们可以指定一个具体的IP地址,如果要响应所有接口上的连接请求就要使用一个特殊的地址INADDR_ANY
  • #define INADDR_ANY (uint32_t)0x00000000
// 监听所有服务器上ip所得到的连接请求
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_addr.s_addr = INADDR_ANY;

tcp示例

服务器端代码
#include <stdio.h>
// atoi函数
#include <stdlib.h>
// I/O函数
#include <unistd.h>
#include <time.h>
// socket 系列函数
#include <sys/socket.h>
// sockaddr_in 结构体
#include <netinet/in.h>
// hton 函数
#include <arpa/inet.h>
#include <signal.h>
// memset函数
#include <string.h>
// 服务器socket描述符
int server_fd = 0;
// 服务器连接属性
struct sockaddr_in server_attr;
// 客户端socket描述符
int client_fd = 0;
// 客户端连接属性
struct sockaddr_in client_attr;
// ctrl + c 关闭服务器监听
void sigint_handle(int);
// 响应客户端连接
void response(int);
// 服务器连接日志
void server_log(struct sockaddr_in*);
int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incorrect parameter\n");
                return 1;
        }
        if(SIG_ERR == signal(SIGINT,sigint_handle))
        {
                perror("signal error");
                return 1;
        }
        memset(&server_attr,0,sizeof(server_attr));
        memset(&client_attr,0,sizeof(server_attr));
        /*
         * 1. 创建socket
         * socket创建在内核中,是一个结构体
         * AF_INET : IPV4
         * SOCK_STREAM : TCP协议
         */
        server_fd = socket(AF_INET,SOCK_STREAM,0);
        if(server_fd == -1)
        {
                perror("sock error");
                return 1;
        }
        /*
         * 2. 调用bind函数将socket与地址(ip,port)进行绑定
         */
        server_attr.sin_family = AF_INET;
        // 转换位网络字节序
        server_attr.sin_port = htons((short)atoi(argv[1]));
        // 表示所有ip,也可以指定第一个ip,指定ip时要把主机字节序转换成网络字节序
        server_attr.sin_addr.s_addr = INADDR_ANY;
        // 绑定ip端口
        if(-1 == bind(server_fd,(struct sockaddr*)&server_attr,sizeof(server_attr)))
        {
                perror("bind error");
                return 1;
        }
        /*
         * 3. 调用listen函数启动监听,通知系统接收来自客户端的请求
         * 第二个参数代表客户端队列的长度
         * 执行成功后能用netstat查看
         */
        if(-1 == listen(server_fd,8))
        {
                perror("listen error");
                return 1;
        }
        socklen_t addrlen = sizeof(client_attr);
        /*
         * 4. 调用accept获得客户端连接,并返回一个新的socket文件描述符
         * 若没有客户端连接,此函数会阻塞,直到获得一个客户端连接
         */
        while(1)
        {
                client_fd = accept(server_fd,(struct sockaddr*)&client_attr,&addrlen);
                server_log(&client_attr);
                /*
                 * 5. 调用I/O函数(read/write)和连接的客户端进行双向的通信
                 */
                response(client_fd);
                close(client_fd);
        }
        close(server_fd);
}
void sigint_handle(int sig)
{
        close(server_fd);
        exit(0);
}
void response(int fp)
{
        time_t rawtime;
        struct tm* timeinfo;
        time(&rawtime);
        timeinfo = localtime(&rawtime);
        char str[39] = "I am server. ";
        strcat(str,asctime(timeinfo));
        write(fp,str,sizeof(str));
}
void server_log(struct sockaddr_in* client_attr)
{
        char ip[16] = {'\0'};
        // 结构体的的数据不能直接显示,需要先转换成本机字节序
        inet_ntop(AF_INET,&client_attr->sin_addr.s_addr,ip,sizeof(ip));
        printf("connected by %s:%d\n",ip,ntohs(client_attr->sin_port));
}
服务器端代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main(int argc,char* argv[])
{
        if(argc < 3)
        {
                printf("incorrect parameter\n");
                return 1;
        }
        int client_fd = socket(AF_INET,SOCK_STREAM,0);
        struct sockaddr_in client_attr;
        memset(&client_attr,0,sizeof(client_attr));
        client_attr.sin_family = AF_INET;
        // 主机字节序转换成网络字节序
        client_attr.sin_port = htons((short)atoi(argv[2]));
        inet_pton(AF_INET,argv[1],&client_attr.sin_addr.s_addr);
        // client_attr.sin_addr.s_addr = 
        if(-1 == connect(client_fd,(struct sockaddr*)&client_attr,sizeof(client_attr)))
        {
                perror("connect ");
                return 1;
        }
        char res[39] = {'\0'};
        read(client_fd,res,sizeof(res));
        printf("buf = %s\n",res);
        close(client_fd);
        return 0;
}
自定义协议服务器端,多进程处理并发
#include <stdio.h>
// atoi函数
#include <stdlib.h>
// wait函数
#include <sys/wait.h>
// I/O函数
#include <unistd.h>
#include <time.h>
// socket 系列函数
#include <sys/socket.h>
// sockaddr_in 结构体
#include <netinet/in.h>
// hton 函数
#include <arpa/inet.h>
#include <signal.h>
// memset函数
#include <string.h>
// 服务器socket描述符
int server_fd = 0;
// 服务器连接属性
struct sockaddr_in server_attr;
// 客户端socket描述符
int client_fd = 0;
// 客户端连接属性
struct sockaddr_in client_attr;
// ctrl + c 关闭服务器监听
void sigint_handle(int);
//  回收子进程方法
void sigchld_handle(int);
// 响应客户端连接
void response(int);
// 服务器连接日志
void server_log(struct sockaddr_in*);
// 自定义写
int my_write(int fd,char* str);
// 自定义读
int my_read(int fd,char* str);
/* 自定义协议结构体 */
typedef struct{
        int bytes;
        char data[512];
}my_protocol;
int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incorrect parameter\n");
                return 1;
        }
        if(SIG_ERR == signal(SIGINT,sigint_handle))
        {
                perror("signal error");
                return 1;
        }
        memset(&server_attr,0,sizeof(server_attr));
        memset(&client_attr,0,sizeof(server_attr));
        /*
         * 1. 创建socket
         * socket创建在内核中,是一个结构体
         * AF_INET : IPV4
         * SOCK_STREAM : TCP协议
         */
        server_fd = socket(AF_INET,SOCK_STREAM,0);
        if(server_fd == -1)
        {
                perror("sock error");
                return 1;
        }
        /*
         * 2. 调用bind函数将socket与地址(ip,port)进行绑定
         */
        server_attr.sin_family = AF_INET;
        // 转换位网络字节序
        server_attr.sin_port = htons((short)atoi(argv[1]));
        // 表示所有ip,也可以指定第一个ip,指定ip时要把主机字节序转换成网络字节序
        server_attr.sin_addr.s_addr = INADDR_ANY;
        // 绑定ip端口
        if(-1 == bind(server_fd,(struct sockaddr*)&server_attr,sizeof(server_attr)))
        {
                perror("bind error");
                return 1;
        }
        /*
         * 3. 调用listen函数启动监听,通知系统接收来自客户端的请求
         * 第二个参数代表客户端队列的长度
         * 执行成功后能用netstat查看
         */
        if(-1 == listen(server_fd,8))
        {
                perror("listen error");
                return 1;
        }
        socklen_t addrlen = sizeof(client_attr);
        /*
         * 4. 调用accept获得客户端连接,并返回一个新的socket文件描述符
         * 若没有客户端连接,此函数会阻塞,直到获得一个客户端连接
         */
        int pid = 0;
        // 注册SIGCHLD信号处理函数,回收子进程,避免僵尸进程的产生
        signal(SIGCHLD,sigchld_handle);
        while(1)
        {
                client_fd = accept(server_fd,(struct sockaddr*)&client_attr,&addrlen);
                if(client_fd <= 0)
                {
                        continue;
                }
                server_log(&client_attr);
                // 子线程处理I/O,达到并发的效果
                pid = fork();
                if(pid == 0)
                {
                        /*
                        * 5. 调用I/O函数(read/write)和连接的客户端进行双向的通信
                        */
                        response(client_fd);
                        close(client_fd);
                        break;
                }
                close(client_fd);
        }
        close(server_fd);
        return 0;
}
void sigint_handle(int sig)
{
        close(server_fd);
        exit(0);
}
void response(int fp)
{
        time_t rawtime;
        struct tm* timeinfo;
        char client_data[512];
        int read_len = 0;
        while(1)
        {
                memset(client_data,'\0',sizeof(client_data));
                time(&rawtime);
                timeinfo = localtime(&rawtime);
                if((read_len = my_read(fp,client_data)) == -1)
                {
                        break;
                }
                else if(read_len == 0)
                {
                        strcat(client_data,"incorrect data\n");
                }
                else
                {
                        strcat(client_data,"\n");
                        strcat(client_data,asctime(timeinfo));
                }
                if(my_write(fp,client_data) == -1)
                {
                        break;
                }
        }
}
void server_log(struct sockaddr_in* client_attr)
{
        char ip[16] = {'\0'};
        // 结构体的的数据不能直接显示,需要先转换成本机字节序
        inet_ntop(AF_INET,&client_attr->sin_addr.s_addr,ip,sizeof(ip));
        printf("connected by %s:%d\n",ip,ntohs(client_attr->sin_port));
}
int my_write(int fd,char* str)
{
        my_protocol p = {0};
        memset(&p.data,'\0',sizeof(p.data));
        strcpy(p.data,str);
        p.bytes = strlen(p.data);
        if(write(fd,&p,sizeof(p)) < 0)
        {
                return -1;
        }
        return 0;
}
int my_read(int fd,char* str)
{
        my_protocol p = {0};
        memset(&p.data,'\0',sizeof(p.data));
        int read_len = 0;
        read_len = read(fd,&p,sizeof(p));
        if(p.bytes != strlen(p.data) && read_len != 0)
        {
                return -1;
        }
        strcpy(str,p.data);
        return read_len;
}
//  回收子进程方法
void sigchld_handle(int sig)
{
        // signal(SIGCHLD,sigchld_handle);
        wait(NULL);
        printf("free\n");
}
自定义协议客户端
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
// 自定义写
int my_write(int fd,char* str);
// 自定义读
int my_read(int fd,char* str);
/* 自定义协议结构体 */
typedef struct{
        int bytes;
        char data[512];
}my_protocol;
int main(int argc,char* argv[])
{
        if(argc < 3)
        {
                printf("incorrect parameter\n");
                return 1;
        }
        int client_fd = socket(AF_INET,SOCK_STREAM,0);
        struct sockaddr_in client_attr;
        memset(&client_attr,0,sizeof(client_attr));
        client_attr.sin_family = AF_INET;
        // 主机字节序转换成网络字节序
        client_attr.sin_port = htons((short)atoi(argv[2]));
        inet_pton(AF_INET,argv[1],&client_attr.sin_addr.s_addr);
        // client_attr.sin_addr.s_addr = 
        if(-1 == connect(client_fd,(struct sockaddr*)&client_attr,sizeof(client_attr)))
        {
                perror("connect ");
                return 1;
        }
        char res[512];
        while(1)
        {
                memset(res,'\0',sizeof(res));
                scanf("%s",res);
                if(-1 == my_write(client_fd,res))
                {
                        perror("write");
                        break;
                }
                if(my_read(client_fd,res) <= 0)
                {
                        perror("read");
                        break;
                }
                printf("res = %s",res);
        }
        close(client_fd);
        return 0;
}
int my_write(int fd,char* str)
{
        my_protocol p = {0};
        memset(&p.data,'\0',sizeof(p.data));
        strcpy(p.data,str);
        p.bytes = strlen(p.data);
        if(write(fd,&p,sizeof(p)) < 0)
        {
                return -1;
        }
        return 0;
}
int my_read(int fd,char* str)
{
        my_protocol p = {0};
        memset(&p.data,'\0',sizeof(p.data));
        int read_len = 0;
        read_len = read(fd,&p,sizeof(p));
        if(p.bytes != strlen(p.data) && read_len != 0)
        {
                return -1;
        }
        strcpy(str,p.data);
        return read_len;
}
tcp自定义协议服务器端,多线程处理并发
#include <stdio.h>
// atoi函数
#include <stdlib.h>
// wait函数
#include <sys/wait.h>
// I/O函数
#include <unistd.h>
#include <time.h>
// socket 系列函数
#include <sys/socket.h>
// sockaddr_in 结构体
#include <netinet/in.h>
// hton 函数
#include <arpa/inet.h>
#include <signal.h>
// memset函数
#include <string.h>
// pthread库
#include <pthread.h>
// 服务器socket描述符
int server_fd = 0;
// 服务器连接属性
struct sockaddr_in server_attr;
// 客户端socket描述符
int client_fd = 0;
// 客户端连接属性
struct sockaddr_in client_attr;
// ctrl + c 关闭服务器监听
void sigint_handle(int);
//  回收子进程方法
void sigchld_handle(int);
// 响应客户端连接
void response(int);
// 服务器连接日志
void server_log(struct sockaddr_in*);
// 自定义写
int my_write(int fd,char* str);
// 自定义读
int my_read(int fd,char* str);
// 多线程处理并发函数
void* thread_handle(void* data);
/* 自定义协议结构体 */
typedef struct{
        int bytes;
        char data[512];
}my_protocol;
int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incorrect parameter\n");
                return 1;
        }
        if(SIG_ERR == signal(SIGINT,sigint_handle))
        {
                perror("signal error");
                return 1;
        }
        memset(&server_attr,0,sizeof(server_attr));
        memset(&client_attr,0,sizeof(server_attr));
        /*
         * 1. 创建socket
         * socket创建在内核中,是一个结构体
         * AF_INET : IPV4
         * SOCK_STREAM : TCP协议
         */
        server_fd = socket(AF_INET,SOCK_STREAM,0);
        if(server_fd == -1)
        {
                perror("sock error");
                return 1;
        }
        /*
         * 2. 调用bind函数将socket与地址(ip,port)进行绑定
         */
        server_attr.sin_family = AF_INET;
        // 转换位网络字节序
        server_attr.sin_port = htons((short)atoi(argv[1]));
        // 表示所有ip,也可以指定第一个ip,指定ip时要把主机字节序转换成网络字节序
        server_attr.sin_addr.s_addr = INADDR_ANY;
        // 绑定ip端口
        if(-1 == bind(server_fd,(struct sockaddr*)&server_attr,sizeof(server_attr)))
        {
                perror("bind error");
                return 1;
        }
        /*
         * 3. 调用listen函数启动监听,通知系统接收来自客户端的请求
         * 第二个参数代表客户端队列的长度
         * 执行成功后能用netstat查看
         */
        if(-1 == listen(server_fd,8))
        {
                perror("listen error");
                return 1;
        }
        socklen_t addrlen = sizeof(client_attr);
        /*
         * 4. 调用accept获得客户端连接,并返回一个新的socket文件描述符
         * 若没有客户端连接,此函数会阻塞,直到获得一个客户端连接
         */
        pthread_t tid = 0;
        // 初始化线程属性,用来创建分离的线程,使子线程结束后自动回收
        pthread_attr_t thread_attr;
        pthread_attr_init(&thread_attr);
        pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
        // 注册SIGCHLD信号处理函数,回收子进程,避免僵尸进程的产生
        signal(SIGCHLD,sigchld_handle);
        // 忽略SIGPIPE信号处理函数,避免由于客户端断开连接,并由于read发送SIGPIPE信号导致进程结>束
        signal(SIGPIPE,SIG_IGN);
        while(1)
        {
                client_fd = accept(server_fd,(struct sockaddr*)&client_attr,&addrlen);
                if(client_fd <= 0)
                {
                        continue;
                }
                server_log(&client_attr);
                // 子线程处理I/O,达到并发的效果
                pthread_create(&tid,&thread_attr,thread_handle,(void*)client_fd);
                        printf("child thread\n");
                        // break;
                // 线程是共享的进程资源,这里不要关闭,不然线程里面的也没了
                // close(client_fd);
        }
        // close(server_fd);
        sleep(3);
        return 0;
}
void sigint_handle(int sig)
{
        close(server_fd);
        close(client_fd);
        exit(0);
}
void response(int fp)
{
        time_t rawtime;
        struct tm* timeinfo;
        char client_data[512];
        int read_len = 0;
        while(1)
        {
                memset(client_data,'\0',sizeof(client_data));
                time(&rawtime);
                timeinfo = localtime(&rawtime);
                if((read_len = my_read(fp,client_data)) == -1)
                {
                        break;
                }
                else if(read_len == 0)
                {
                        strcat(client_data,"incorrect data\n");
                }
                else
                {
                        strcat(client_data,"\n");
                        strcat(client_data,asctime(timeinfo));
                }
                if(my_write(fp,client_data) == -1)
                {
                        break;
                }
        }
}
void server_log(struct sockaddr_in* client_attr)
{
        char ip[16] = {'\0'};
        // 结构体的的数据不能直接显示,需要先转换成本机字节序
        inet_ntop(AF_INET,&client_attr->sin_addr.s_addr,ip,sizeof(ip));
        printf("connected by %s:%d\n",ip,ntohs(client_attr->sin_port));
}
int my_write(int fd,char* str)
{
        my_protocol p = {0};
        memset(&p.data,'\0',sizeof(p.data));
        strcpy(p.data,str);
        p.bytes = strlen(p.data);
        if(write(fd,&p,sizeof(p)) < 0)
        {
                return -1;
        }
        return 0;
}
int my_read(int fd,char* str)
{
        my_protocol p = {0};
        memset(&p.data,'\0',sizeof(p.data));
        int read_len = 0;
        read_len = read(fd,&p,sizeof(p));
        if(p.bytes != strlen(p.data) && read_len != 0)
        {
                return -1;
        }
        strcpy(str,p.data);
        return read_len;
}
//  回收子进程方法
void sigchld_handle(int sig)
{
        // signal(SIGCHLD,sigchld_handle);
        wait(NULL);
        printf("free\n");
}
void* thread_handle(void* data)
{
        int fd = (int)data;
        /*
        * 5. 调用I/O函数(read/write)和连接的客户端进行双向的通信
        */
        response(fd);
        close(fd);
        return (void*)NULL;
}

udp编程

udp编程模型

  • 客户端调用序列

    • 调用socket函数创建套接字
    • 调用bind连接服务器端(这一步可以不要)
    • 调用sendto/readfrom与服务器端通讯
    • 调用close关闭套接字
  • 服务器端调用序列

    • 调用socket函数创建套接字
    • 调用bind绑定本地地址和端口
    • 调用sendto/readfrom与客户端通讯
    • 调用close关闭套接字

数据传输

发送数据

ssize_t send(int sockfd,const void* buf,size_t nbytes,int flag);

  • 返回:成功返回发送字节数,出错返回-1
  • 功能:发送数据,需要在发送数据前已经建立的连接,多用于TCP,用于UDP时要使用connect函数先建立连接
    `ssize_t sendto(int sockfd,const void buf,size_t nbytes,int flag,const struct sockaddr

desaddr,socklen_t destlen);`

  • 返回:成功返回发送的字节数,出错返回-1
  • 功能:发送数据,需要指定发送方(desaddr参数)
  • sockaddr:目的地的相关信息
    ssize_t sendmsg(int sockfd,const struct msghdr* msg,int flag);
  • 返回:成功返回发送的字节数,出错返回-1
  • 头文件
#include <sys/socket.h>
  • msghdr结构体
struct msghdr
{
        void* msg_name;// optional address
        socklen_t msg_namelen;// address size in bytes
        struct iovec* msg_iov;// array of I/O buffers
        int msg_iovlen;// number of elements in array
        void* msg_control;// ancillary data
        socklen_t msg_controllen;// number of ancillary bytes
        int msg_flags;// flag for received message
};
接收数据

ssize_t recv(int sockfd,void* buf,size_t nbytes,int flag);

  • 功能:接收数据,不能获得发送方的信息,
    ssize_t recvfrom(int sockfd,void*restrict buf,size_t len,int flag,struct sockaddr*restrict addr,socklen_t*restrict addrlen);
  • 功能:除了能获取数据外,还可以获得发送方的信息,用于在之后对指定的发送方发送数据
    ssize_t recvmsg(int sockfd,struct msghdr* msg,int flag);
  • 头文件
#include <sys/socket.h>

示例

服务器端
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <pthread.h>
typedef struct
{
        struct sockaddr_in attr;
        char data[512];
}thread_data;
int server_fd = 0;
void* thread_handle(void* client);
void server_log(struct sockaddr_in* attr);
int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incorrect parametr\n");
                return 1;
        }
        /*
         * 1. 创建socket
         * SOCK_DGRAM : 使用UDP传输
         */
        server_fd = socket(AF_INET,SOCK_DGRAM,0);

        /*
         * 2. 设置服务器选项,SO_REUSEADDR,会先解除之前的绑定,在绑定一次,避免重启时因为连接还
没有断开导致的bind报错
         */
        setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,(void*)NULL,0);

        /*
         * 3. 设置服务器选项,SO_SNDTIMEO,发送客户端消息的超时时间,避免子线程阻塞
         */
        struct timeval time_out = {3,0};
        // setsockopt(server_fd,SOL_SOCKET,SO_RCVTIMEO,(char*)&time_out,sizeof(time_out));
        setsockopt(server_fd,SOL_SOCKET,SO_SNDTIMEO,(char*)&time_out,sizeof(time_out));

        /*
         * 4. 设置绑定的ip及端口信息
         */
        struct sockaddr_in server_attr;
        server_attr.sin_family = AF_INET;
        server_attr.sin_port = htons((short)atoi(argv[1]));
        server_attr.sin_addr.s_addr = INADDR_ANY;

        /*
         * 5. 绑定本地ip,port
         */
        bind(server_fd,(struct sockaddr*)&server_attr,sizeof(server_attr));
        // 存放客户端信息
        thread_data* client;
        // 存放子线程id
        pthread_t tid = 0;
        // 存放子线程属性,用以设置以分离模式启动子线程
        pthread_attr_t thread_attr;
        memset(&thread_attr,0,sizeof(thread_attr));
        pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
        // 上次是否读取成功的标识,用以判断是否需要重新申请内存
        int recv_flag = 0;
        // 连接信息结构体的大小
        socklen_t addrlen = sizeof(struct sockaddr_in);
        while(1)
        {
                /*
                * 6. 获得客户端发送的数据与客户端信息,用于与客户端通信
                */
                if(recv_flag == 0)
                {
                        client = malloc(sizeof(thread_data));
                }
                memset(client,0,sizeof(struct sockaddr_in));
                if(0 >= recvfrom(server_fd,client->data,sizeof(client->data),0,(struct sockaddr*)&client->attr,&addrlen))
                {
                        // 超时或者读取失败则继续下一次等待,设置标识,避免下次重新申请内存
                        printf("time out\n");
                        recv_flag = 1;
                        continue;
                }
                server_log(&client->attr);
                /*
                * 7. 使用子线程处理并发
                */
                pthread_create(&tid,&thread_attr,thread_handle,(void*)client);
                recv_flag = 0;
        }
        return 0;
}
void* thread_handle(void* client)
{
        thread_data* t = (thread_data*)client;
        char thread_id[20] = {'\0'};
        sprintf(thread_id,"%lu",pthread_self());
        // 连接信息结构体的大小
        strcat(t->data,"\n;thread_id = ");
        strcat(t->data,thread_id);
        sendto(server_fd,t->data,sizeof(t->data),0,(struct sockaddr*)&t->attr,sizeof(t->attr));
        // 释放动态申请的内存
        free(client);
        return NULL;
}
void server_log(struct sockaddr_in* attr)
{
        char ip[16] = {'\0'};
        inet_ntop(AF_INET,&attr->sin_addr.s_addr,ip,sizeof(ip));
        printf("connect from %s handle by %lu\n",ip,pthread_self());
}
udp客户端
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <pthread.h>
int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incorrect parametr\n");
                return 1;
        }
        /*
         * 1. 创建socket
         * SOCK_DGRAM : 使用UDP传输
         */
        int client_fd = socket(AF_INET,SOCK_DGRAM,0);


        /*
         * 3. 设置服务器的ip及端口信息
         */
        struct sockaddr_in server_attr;
        server_attr.sin_family = AF_INET;
        server_attr.sin_port = htons((short)atoi(argv[2]));
        inet_pton(AF_INET,argv[1],&server_attr.sin_addr.s_addr);
        char res[512] = {'\0'};
        while(1)
        {
                scanf("%s",res);
                if(sendto(client_fd,res,sizeof(res),0,(struct sockaddr*)&server_attr,sizeof(server_attr)) <= 0)
                {
                        perror("sendto");
                        break;
                }
                if(recv(client_fd,res,sizeof(res),0) <= 0)
                {
                        perror("recv");
                        break;
                }
                printf("%s\n",res);
        }
        return 0;
}

域名解析函数

hostent结构体

struct hostent
{
        char* h_name;// 主机名
        char** h_aliases;// 别名,字符串数组
        int h_addrtype;// 协议类型
        int h_length;// 网络地址大小
        char** h_addr_list;// 指向网络地址的指针
};

函数

struct hostent* gethostent(void);
struct hostent* gethostbyname(const char* hostname);
void sethostent(int stayopen);
void endhostent(void); // 与hostent一起使用

  • 头文件
#include <netdb.h>

示例

if((hptr = gethostbyname("www.jinblog.com")) == NULL)
{
        fprintf(stderr,"gethostbyname call faile. %s\n",hsterror(h_errno));
        return 1;
}
printf("official name:%s\n",hptr->h_name);
for(pptr = hptr->h_aliases;*pptr != NULL;pptr++)
{
        printf("\talias:%s\n",*pptr);
}
if(hptr->h_addrtype != AF_INET)
{
        fprintf(stderr,"invalid address type %d\n",hptr->h_addrtype);
}
pptr = hptr->h_addr_list;
for(;*pptr != NULL;pptr++)
{
        printf("\taddress:%s\n",inet_ntop(hptr->h_addrtype,*pptr,str,sizeof(str)));
}
打印
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
void print_host(struct hostent* host);
int main(void)
{
        printf("----------gethostent start--------\n");
        struct hostent* host;
        while((host = gethostent()) != NULL)
        {
                print_host(host);
        }
        endhostent();
        printf("----------gethostent  stop--------\n");
        
        printf("\n\n----------gethostentbyname start--------\n");

        if((host = gethostbyname("jinblog.com")) != NULL)
        {
                print_host(host);
        }

        printf("----------gethostentbyname  stop--------\n");
        return 0;
}
void print_host(struct hostent* host)
{
        printf("host : %s\n",host->h_name);
        int i = 0;
        while(host->h_aliases[i] != NULL)
        {
                printf("\talias : %s\n",host->h_aliases[i]);
                i++;
        }
        switch(host->h_addrtype)
        {
                case AF_INET:
                        printf("address type : AF_INET\n");
                        break;
                case AF_INET6:
                        printf("address type : AF_INET6\n");
                        break;
                case AF_UNIX:
                        printf("address type : AF_UNIX\n");
                        break;
                default:
                        printf("address type : unknown\n");
                        break;
        }
        printf("length : %d\n",host->h_length);
        i = 0;
        char ip[32] = {'\0'};
        while(host->h_addr_list[i] != NULL)
        {
                inet_ntop(host->h_addrtype,host->h_addr_list[i],ip,sizeof(ip));
                printf("\tip : %s",ip);
                i++;
                memset(ip,'\0',sizeof(ip));
        }
        printf("\n\n\n");
}

广播

  • 实现一对多的通讯
  • 通过向广播地址发送数据报文实现

套接字选项

  • 套接字选项用于修饰套接字以及其底层通讯协议的各种行为。函数setsockopt和getsockopt可以查看和设置套接字的各种选项
    int getsockopt(int sockfd,int optname,void* optval,socklen_t* optlen);

int setsockopt(int sockfd,int optname,void* optval,socklen_t* optlen);

SO_BROADCAST选项

  • SO_BROADCAST选项控制着UDP套接字是否能够发送广播数据报,选项的类型位int,非零意味着是,注意,只有UDP套接字可以使用这个选项,TCP是不能使用广播的
int opt = 1;
if((sockfd = sock(AF_INET,SOCK_DGRAM,0)) < 0)
{
        // 错误处理
}
if(setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&opt,sizeof(int)) < 0)
{
        // 错误处理
}

SO_SNDBUF和SO_RCVBUF选项

  • 每个套接字有一个发送缓冲区和接收缓冲区,这两个缓冲区由底层协议使用,接收缓冲区存放由协议接收的数据直到被应用程序读走,发送缓冲区存放应用写出的数据直到被协议发送出去。SO_SNDBUF和SO_RCVBUF选项分别控制发送和接收缓冲区的大小,他们类型为int,以字节为单位
if((sockfd = sock(AF_INET,SOCK_DGRAM,0)) < 0)
{
        // 错误处理
}
int opt = 0;
if(getsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&opt,sizeof(opt)) < 0)
{
        // 错误处理
}
opt += 888;
if(setsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&opt,sizeof(opt)) < 0)
{
        // 错误处理
}

广播地址

  • 如果用{netID,subnetID,hostID}({网络id,子网id,主机id})标识IPV4地址,那么有四类的广播地址,我们用-1表示所有比特都为1的字段
  • 子网广播地址:{netID,subnetID,-1},这类地址编排指定子网上的所有接口,例如,如果我们对B类地址192.168采用8为子网id,那么192.168.2.255将是192.168.2子网上的所有接口的子网广播地址。路由器不转发这类广播
  • 全部子网地址:{netID,-1,-1}。这类广播地址编排指定网络上的所有子网。如果说这类地址被使用过的话,那么现在已很少见了
  • 受限广播地址{-1,-1,-1}或255.255.255.255。路由器从不转发目的地址为255.255.255.255的IP报数据。

示例

广播服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc,char* argv[])
{
        if(argc < 3)
        {
                printf("incrrect parameter\n");
                return 1;
        }
        /*
         * 1. 创建socket,要创建UDP类型的socket,广播包是用UDP发送的
         */
        int server_fd = socket(AF_INET,SOCK_DGRAM,0);

        /*
         * 2. 设置要发送到的广播域和port
         */
        struct sockaddr_in des_sock_attr;
        des_sock_attr.sin_family = AF_INET;
        // 注意argv[1]是广播地址
        inet_pton(AF_INET,argv[1],&des_sock_attr.sin_addr.s_addr);
        des_sock_attr.sin_port = htons((short)atoi(argv[2]));

        /*
         * 2. 设置socket选项,使socket可以发送广播报
         */
        int opt = 1;
        setsockopt(server_fd,SOL_SOCKET,SO_BROADCAST,&opt,sizeof(int));
        char buf[512] = {'\0'};
        int i = 0;
        while(1)
        {
                sprintf(buf,"message %d\n",i);
                if(0 >= sendto(server_fd,buf,sizeof(buf),0,(struct sockaddr*)&des_sock_attr,sizeof(des_sock_attr)))
                {
                        perror("sendto");
                        continue;
                }
                printf("broadcast : %d\n",i);
                sleep(3);
                i++;
                memset(buf,'\0',sizeof(buf));
        }
        return 1;
}
广播客户端
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc,char* argv[])
{
        if(argc < 2)
        {
                printf("incrrect parameter\n");
                return 1;
        }
        /*
         * 1. 创建socket,要创建UDP类型的socket,广播包是用UDP发送的
         */
        int client_fd = socket(AF_INET,SOCK_DGRAM,0);
        /*
         * 2. 设置socket,ip,port等属性
         */
        struct sockaddr_in client_attr;
        client_attr.sin_port = htons((short)atoi(argv[1]));
        client_attr.sin_addr.s_addr = INADDR_ANY;
        client_attr.sin_family = AF_INET;

        /*
         * 3. 设置 SO_REUSEADDR,避免bind报错
         */
        setsockopt(client_fd,SOL_SOCKET,SO_REUSEADDR,(void*)NULL,0);
        /*
         * 4. bind绑定ip端口
         */
        bind(client_fd,(struct sockaddr*)&client_attr,sizeof(client_attr));

        /*
         * 5. 接收广播报数据
         */
        struct sockaddr_in server_attr;
        char buf[512] = {'\0'},ip[16] = {'\0'};
        socklen_t addrlen = sizeof(server_attr);
        while(1)
        {
                if(recvfrom(client_fd,buf,sizeof(buf),0,(struct sockaddr*)&server_attr,&addrlen) <= 0)
                {
                        perror("recvfrom");
                        return 1;
                }
                inet_ntop(AF_INET,&server_attr.sin_addr.s_addr,ip,sizeof(ip));
                printf("server ip : %s;\nbroadcast data : %s\n\n",ip,buf);
        }
        return 0;
}

下一篇:http://jinblog.com/archives/868.html