《TCP/IP网络编程》三、基于Linux的编程

来源:互联网 发布:淘宝禁用词表 编辑:程序博客网 时间:2024/05/20 16:45

  • 套接字和标准IO
      • 标准IO函数优点
      • 标准IO函数缺点
      • 转换函数
  • 关于IO流分离
      • 两次IO分流
      • 分离流的好处
      • 文件描述符的复制和半关闭
  • 优于select的epoll
      • 基于select的IO复用技术速度慢的原因
      • epoll如何优化select问题
      • 实现epoll时必要的函数和结构体
      • 条件触发和边缘触发
      • 将epoll改为边缘触发
  • 多线程服务器端的实现
      • 线程基本概念
      • 线程和进程的差异
      • 线程的创建及运行
      • 线程同步
      • 线程的销毁

15.套接字和标准I/O

1.标准I/O函数优点

良好的移植性。良好移植性这个不需多解释,不仅是I/O函数,所有的标准函数都具有良好的移植性。因为,为了支持所有的操作系统(编译器),这些函数都是按照ANSI C标准定义的。

标准I/O函数可以利用缓冲提高性能。在网络通信中,read,write传输数据只有一种套接字缓冲,但使用标准I/O传输会有额外的缓冲,即I/O缓冲和套接字缓冲两个。使用I/O缓冲主要是为了提高性能,需要传输的数据越多时越明显。因为,一次发送更多的数据要比分多次发送同样的数据性能要高。发送一次数据就对应一个数据包,往往数据包的头信息比较大,它与数据大小无关。

2.标准I/O函数缺点

不容易进行双向通信。

有时可能频繁调用fflush函数。

需要以FILE结构体指针的形式返回文件描述符

3.转换函数

通过fdopen函数将创建套接字时返回的文件描述符转换为标准I/O函数中使用的FILE结构体指针

通过fileno将FILE结构体指针转换为文件描述符

16.关于I/O流分离

1.两次I/O分流

1)fork函数复制出文件描述符,以区分输入和输出中使用的文件描述符
2)通过两次fdopen函数的调用,创建读模式FILE指针和写模式FILE指针,分离输入和输出工具

2.分离“流”的好处

第一种分离方式:
1.分开输入输出过程降低实现难度(简单易维护)
2.与输入无关的输出操作可以提高速度(阻断函数)

第二种分离方式:
1.转换为FILE指针文件操作按读模式与写模式区分
2.区分读写模式降低实现难度
3.区分I/O缓冲提高缓冲性能

3.文件描述符的复制和半关闭

在创建FILE指针前先复制一份原文件描述符即可进入“可以输入但无法输出”的半关闭状态,这样原文件描述符和副本文件描述符都引用同一个套接字,这时调用fclose函数关闭其中一个也不会销毁套接字(因为销毁所有文件描述符后才能销毁套接字),实现半关闭环境,然后调用shutdown半关闭套接字。
这里写图片描述
这里写图片描述
调用shutdown函数时,无论复制出多少文件描述符都进入半关闭状态,同时传递EOF。

复制文件描述符的方法:

#include<unistd.h>int dup(int fildes); //复制文件描述符fileds int dup2(int fildes, int fildes2); //将文件描述符fildes复制并指定描述符为fildes2

17.优于select的epoll

1.基于select的I/O复用技术速度慢的原因:

1)调用select函数后常见的针对所有文件描述符的循环语句。它每次事件发生需要遍历所有文件描述符,找出发生变化的文件描述符
2)每次调用select函数时都需要向该函数传递监视对象信息。即每次调用select函数时向操作系统传递监视对象信息,至于为什么要传?是因为我们监视的套接字变化的函数,而套接字是操作系统管理的。(这个才是最耗效率的)

注:基于这样的原因并不是说select就没用了,在这样的情况下就适合选用select:1)服务端接入者少 2)程序应具有兼容性。

2.epoll如何优化select问题

1)每次发生事件它不需要循环遍历所有文件描述符,它把发生变化的文件描述符单独集中到了一起。
2)仅向操作系统传递1次监视对象信息,监视范围或内容发生变化时只通知发生变化的事项。

优点:
1)无需编写以监视状态变化为目的的针对所有文件描述符的循环语句
2)调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息

3.实现epoll时必要的函数和结构体

epoll_create:创建保存epoll文件描述符的空间,该函数也会返回文件描述符,所以终止时,也要调用close函数。(创建内存空间)

epoll_ctl:向空间注册,添加或修改文件描述符。(注册监听事件)

epoll_wait:与select函数类似,等待文件描述符发生变化。(监听事件回调)

结构体:

struct epoll_event { __uint32_t events; epoll_data_t data; }typedef union epoll_data { void *ptr; int fd; __uinit32_t u32; __uint64_t u64; } epoll_data_t;

4.条件触发和边缘触发

条件触发和边缘触发是指事件响应的方式,epoll默认是条件触发的方式。
条件触发是指:只要输入缓冲中有数据就会一直通知该事件,循环响应epoll_wait。
边缘触发是指:输入缓冲收到数据时仅注册1次该事件,即使输入缓冲中还留有数据,也不会再进行注册,只响应一次。

边缘触发相对条件触发的优点:可以分离接收数据和处理数据的时间点,从实现模型的角度看,边缘触发更有可能带来高性能。

5.将epoll改为边缘触发:

1)首先改写 event.events = EPOLLIN | EPOLLET; (EPOLLIN:读取数据事件 EPOLLET:边缘触发方式)

2)边缘触发只响应一次接收数据事件,所以要一次性全部读取输入缓冲中的数据,那么就需要判断什么时候数据读取完了?Linux声明了一个全局的变量:int errno; (error.h中),它能记录发生错误时提供额外的信息。这里就可以用它来判断是否读取完数据:

str_len = read(...);if(str_len < 0){    if(errno == EAGAIN) //读取输入缓冲中的全部数据的标志        break;}

3)边缘触发方式下,以阻塞方式工作的read&write有可能会引起服务端的长时间停顿。所以边缘触发一定要采用非阻塞的套接字数据传输形式。那么怎么将套接字的read,write数据传输形式修改为非阻塞模式呢?

//fd套接字文件描述符,将此套接字数据传输模式修改为非阻塞void setnonblockingmode(int fd){    int flag = fcntl(fd, F_GETFL,0); //得到套接字原来属性    fcntl(fd, F_SETFL, flag | O_NONBLOCK);//在原有属性基础上设置添加非阻塞模式}

18.多线程服务器端的实现

1.线程基本概念

多进程服务器开销很大,因此引入线程,可以把它看成是一种轻量级进程。它相比进程有如下几个优点:
线程的创建和上下文切换开销更小且速度更快。
线程间交换数据时无需特殊技术。

2.线程和进程的差异

每个进程的内存空间都由保存全局变量的数据区、向malloc等函数的动态分配提供空间的堆、函数运行时使用的栈构成,每个进程都拥有这种独立空间
这里写图片描述
线程为了保持多条代码执行流把栈区分离出来,进程中的数据区,堆区则共享
这里写图片描述

进程:在操作系统构成单独执行流的单位。
线程:在进程构成单独执行流的单位。

3.线程的创建及运行

线程具有单独的执行流,因此需要单独定义线程的入口函数,而且还需要请求操作系统在单独的执行流中执行该函数,完成这个功能的函数如下:

#include <pthread.h>int pthread_create(    pthread_t * restrict thread,//保存线程ID    const pthread_attr_t * restrict attr,//线程属性,NULL默认属性    void * (* start_routine)(void *), //线程入口函数,函数指针    void * restrict arg //传递给入口函数的参数);

4.线程同步

线程同步用于解决线程访问顺序引发的问题,一般是如下两种情况:
同时访问同一内存空间时发生的情况
需要指定访问同一内存空间的线程执行顺序的情况

1)互斥量
互斥量技术从字面也可以理解,就是临界区有线程访问,其它线程就得排队等待,它们的访问是互斥的,实现方式就是给临界区加锁与释放锁。

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);  //创建互斥量int pthread_mutex_destroy(pthread_mutex_t *mutex);//销毁互斥量int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁int pthread_mutex_unlock(pthread_mutex_t *mutex);//释放锁

简言之,就是利用lock和unlock函数围住临界区的两端。当某个线程调用pthread_mutex_lock进入临界区后,如果没有调用pthread_mutex_unlock释放锁退出,那么其它线程就会一直阻塞在临界区之外,我们把这种情况称之为死锁。所以临界区围住一定要lock和unlock一一对应。

2)信号量
信号量与互斥量类似,只是互斥量是用锁来控制线程访问而信号量是用二进制0,1来完成控制线程顺序。sem_post信号量加1,sem_wait信号量减1,当信号量为0时,sem_wait就会阻断,因此通过这样让信号量加1减1就能控制线程的执行顺序了。

#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);//创建信号量int sem_destroy(sem_t *sem);//销毁信号量int sem_post(sem_t *sem);//信号量加1int sem_wait(sem_t *sem);//信号量减1,为0时阻塞

5.线程的销毁

线程创建后并不是其入口函数返回后就会自动销毁,需要手动销毁,不然线程创建的内存空间将一直存在。一般手动销毁有如下两种方式:
1)调用pthread_join函数,其返回后同时销毁线程 ,是一个阻断函数,服务端一般不用它销毁,因为服务端主线程不宜阻断,要实时监听客服端连接。

2)调用pthread_detach函数,不会阻塞,线程返回自动销毁线程,不过要注意调用它后不能再调用pthread_join函数,它与pthread_join主要区别就是一个是阻塞函数,一个不阻塞。

原创粉丝点击