Linux高性能服务器编程-游双——第八章 高性能服务器框架

服务器一般可以解构为三个主要模块

  • I/O处理单元:四种I/O模型,两种高效时间处理模式
  • 逻辑单元:两种高效并发模式,逻辑状态机
  • 存储单元:略

同步需要按顺序执行,执行不了就硬等,串行;异步方法一旦开始,立即返回,调用者无需等待其中方法执行完成,就可以继续执行后续方法,并发。

8.1 服务器模型

8.1.1 C/S模型

  • C/S(client/server,客户/服务器模型)。
  • 逻辑:服务器启动,创建一个或多个监听socket,调用bind函数绑定到特定端口,调用listen函数等待客户连接。客户端可以调用connect函数像服务器发起连接。
  • 因为客户端的连接请求是随机到达的异步事件,需要某种I/O模型来监听这一事件。
  • 下图展示select系统调用的I/O复用技术,
    在这里插入图片描述
  • C/S模型适合资源相对集中的场合,实现简单。缺点在于访问量过大时,用户得到的响应可能变慢。

8.1.2 P2P模型

  • 所有主机地位对等,每台机器在消耗服务的同时,也给其他机器提供服务。
  • 传统的P2P模型主机之间很难发现,所以P2P模型通常有一个发现服务器,专门用来提供给查找服务,使得客户能够很快找到自己的资源。
    在这里插入图片描述
  • 从编程角度,P2P可以看成C/S模型的扩展,每台主机既是C又是S。

8.2 服务器编程框架

  • I/O处理单元:
    • 管理客户连接模块。
    • 等待并接收新的客户连接
    • 接收客户数据
    • 将服务器响应返回给客户端
    • 数据的收发也可能在逻辑单元中执行。
  • 逻辑单元:
    • 通常是一个进程或线程
    • 分析并处理客户数据,结果传递给I/O处理单元或直接发送给客户端。
    • 对于多个逻辑单元的服务器,可以实现并行处理。
  • 存储单元:
    • 数据库、缓存、文件、独立的服务器
    • 并不是必须的。例如ssh和telnet就不需要
  • 请求队列:
    • 各个单元之间通信方式的抽象。
    • I/O处理单元接收到客户请求时,需要以某种方式通知逻辑单元来处理请求。
    • 多个逻辑单元访问一个存储单元时,也需要以某种机制协调处理静态条件。
    • 请求队列通常被实现为池的一部分。
      在这里插入图片描述

8.3 I/O模型

  • socket创建时默认是阻塞的。
  • 阻塞的文件描述符称为阻塞I/O,非阻塞文件描述符称为非阻塞I/O。
  • 针对阻塞I/O的系统调用可能因为无法立刻完成被系统挂起,直到等待的事件发送。
    • 例如客户端的connect调用,如果服务器的确认文报没有到达客户端,那么connect将会被挂起,直到客户端收到确认文报唤醒。
    • accept、send、recv和connect系统调用都可能会被阻塞。
  • 对于非阻塞I/O的系统调用会立刻返回,不管事件是否发送,如果没发送,返回-1。
  • 在事件已经发生的情况下操作非阻塞I/O才能提高程序效率。因此非阻塞I/O通常要和其他I/O通知机制一起使用。
  • I/O复用

    • 应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。
    • Linux常用I/O复用函数是select、poll、epoll_wait
    • I/O复用本身是阻塞的,因为他们可以同时监听多个I/O事件,所以才提高效率。
    • 阻塞I/O,I/O复用,型号驱动I/O都是同步I/O模型。I/O的读写操作,都是在I/O事件发生后,由应用程序来完成。
    • 对于异步I/O而言,用户可以直接对I/O执行读写操作。
    • 同步I/O向应用程序通知的是I/O就绪事件,异步I/sO向应用程序通知的是I/O完成事件
      在这里插入图片描述

8.4 两种高效的事件处理模式

  • Reactor和Proactor
  • 同步I\O模型通常用于事件Reactor模式
  • 异步I\O通常用于实现Proactor模式,但是同步I\O也能实现Proactor模式。

8.4.1 Reactor模式

  • 主线程(I/O处理单元)只负责监听事件是否发生,事件发生立刻通知到工作线程。
  • 读写数据、接收新连接、处理用户请求均在工作线程完成。
  • 使用同步I/O(例如epoll_wait)实现的Reactor模式:
    • 主线程往epoll内核事件表注册socket可读就绪事件
    • 主线程调用epoll_wait等待socket上有数据可读
    • socket有数据,epoll_wait通知主线程,主线程将socket可读事件放入请求队列。
    • 某个工作线程被唤醒,从socket中读取数据,处理客户请求,往epoll内核事件表注册socket上的写就绪事件
    • 主线程调用epoll_wait等待socket可写
    • 当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列。
    • 某个工作线程被唤醒,往socket写入服务器处理客户请求的结果。
    • 在这里插入图片描述

8.4.2 Proactor模式

  • Proator模式把所有I/O操作交给主线程和内核处理,工作线程处理逻辑业务。
  • 使用异步I/O(aio_read和aio_write)实现的Proactor模式工作流程:
    • 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时候如何通知应用程序。
    • 主线程继续处理其他罗i就
    • 当socket上的数据被读入用户缓冲区后,内核向应用程序发送信号,通知应用程序数据以及可用。
    • 选择一个工作线程处理用户请求。
    • 处理完用户请求,使用aio_write函数向内核注册socket写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序。
    • 主线程继续处理其他逻辑。
    • 当用户缓冲区的数据被写入socket之后,内核向应用程序发送一个信号,通知应用程序数据发送完毕。
    • 应用程序预先定义好的信号处理函数选择一个工作线程来做善后工作,例如决定是否关闭socket。
    • 在这里插入图片描述

8.4.3 模拟Proactor模式

  • 使用同步IO也能模拟Proactor模式,原理在于主线程执行数据读写操作,当读写完成后,主线程向工作线程通知完成事件,所以从工作线程角度看,他直接获得了数据读写的结果,只需要对结果进行逻辑处理。
  • 使用同步IO(例如epoll_wait)模拟Proactor模式的工作流程如下:
    • 主线程往epoll内核事件表注册socket 读就绪事件
    • 主线程调用epoll_wait函数等待socket上有数据可读
    • 当socket上有数据可读时,epoll_wait通知主线程,主线程从socket循环读取数据,将读取到的数据封装成请求并加入请求队列。
    • 请求队列某个工作线程被唤醒,获得请求对象并处理客户请求,之后往epoll内核事件表注册socket写就绪事件、
    • 主线程调用epoll_wait等待socket可写
    • socket可写时,epoll_wait通知主线程,主线程往socket写入处理结果。
    • 在这里插入图片描述

8.5 两种高效的并发模式

  • 计算密集型,并发编程并无优势,反倒由于任务切换使得效率降低。
  • I/O密集型,并发可以提高CPU利用率。

8.5.1 半同步/半异步模式

  • 同步:程序按代码顺序执行
  • 异步:程序执行需要事件驱动
    在这里插入图片描述
  • 同步线程用于处理客户逻辑,异步线程用于处理I/O事件

8.5.1.1 半同步/半反应堆 模式

  • 主线程为异步线程,负责监听socket事件
  • 采用Reactor模式,工作线程自己从socket中读取客户请求,以及往socket中写入服务器应答。
  • 也可以采用Proactor模式
    在这里插入图片描述
  • 缺点:
    • 主线程和工作线程共享请求队列,所以主线程加任务到请求队列,或者工作线程从主线程中取出任务,都要加锁解锁。
    • 工作线程只能处理一个客户请求,客户数量多而工作线程少, 客户端响应速度会降低。
  • 相对高效的半同步半异步:
    • 主线程监听socket,连接socket有工作线程来处理
    • 与上边的半同步半反应堆的区别在于,上边是在主线程的epoll注册事件,这个是在工作线程epoll注册事件
      在这里插入图片描述

8.5.2 领导者/追随者模式

  • 程序只有一个领导者线程,负责监听IO事件,其他线程是追随者,休眠在线程池中等待成为新的领导者。
  • 如果当前领导者线程检测到IO事件,需要首先从线程池中选出新的领导者线程,如何处理IO事件,新的领导者线程等待新的IO事件,而原来的领导者则处理IO事件, 实现并发。

8.6 有限状态机

  • 逻辑单元内部的高效编程方法
  • 状态独立的有限状态机:状态之间没有转移
    在这里插入图片描述
  • 带状态转移的有限状态机:
    • 三种状态type_A,type_B和type_C
    • 状态转移是A->B->C
    STATE_MACHINE(){
    	State cur_state = type_A;
    	while(cur_state != type_C){
    		package _pack = getNewPackage();
    		switch(cur_state){
    			case type_A:
    				process_package_state_A(_pack);
    				cur_state = type_B;
    				break;
    			case type_B:
    				process_package_state_B(_pack);
    				cur_state = type_C;
    				break;
    		}
    	}
    }
    

8.6.1 HTTP请求的读取和分析可以用有限状态转移机实现。

#include "../create_sockfd.h"
#include "stdio.h"
#include "stdlib.h"
#include "errno.h"
//读缓冲区大小,HTTP请求不能超过4096字节
#define BUFFER_SIZE 4096

//主状态机的两种可能的状态,正在分析请求行、正在分析头部字段
enum CHECK_STATE{ CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER};

//从状态机的三种可能状态,也就是读取的行的情况:完整行、行出错、行数据不完整
enum LINE_STATUS{LINE_OK = 0, LINE_BAD, LINE_OPEN};

//服务器处理HTTP请求的结果:
//NO_REQUEST表示请求不完整,需要继续读取客户数据;GET_REQUEST表示获取到了一个完整的客户请求;
//BAD_REQUEST表示客户请求有语法错误;FORBIDDEN_REQUEST表示客户对资源没有足够的访问权限;
//INTERNAL_ERROR表示服务器内部错误;CLOSE_CONNECTION表示客户端已经关闭连接
enum HTTP_CODE {NO_REQUEST = 0, GET_REQUEST, BAD_REQUEST, FORBIDDEN_REQUEST, INTERNAL_ERROR, CLOSE_CONNECTION};

//出于简化考虑,本程序没有给客户端发送完整HTTP报文,只是根据处理结果发送如下成功/失败信息。
static const char* szret[] = {"HTTP/1.1 200 OK\r\nContent-Type: text/html; charser=UTF-8\r\n\r\n", "HTTP/1.1 404 ERROR\r\n\r\n"};


//从状态机,用于解析一行的内容。buffer是需要分析的数据,checked_index指向当前正在分析的字节,read_index指向buffer中客户数据尾部的下一个字节。
//buffer中,0~check_index的数据都已经分析完毕
//一个行的分割,最后字符是"\r\n",而且'\r'后边如果不是'\n',则说明语法有误。
LINE_STATUS parse_line(char* buffer, int& checked_index, int& read_index){
    char temp;
    for(; checked_index < read_index; ++checked_index){
        //当前分析字符
        temp = buffer[checked_index];
        //如果当前字符是'\r',且下一个字符是'\n'则说明是一个完整的行,如果'\r'刚好是最后一个字符,那说明这个行还不完整
        //如果当前字符是'\n',且上一个字符是'\r',说明也是一个完整的行
        if(temp == 'r'){
            //刚好是最后一个字节,说明行不完整
            if((checked_index + 1) == read_index){
                return LINE_OPEN;
            } else if(buffer[checked_index + 1] == '\n'){ //读到了一个完整的行,把原来字符串中的'\r\n'替换成\0\0
                buffer[checked_index++] = '\0';
                buffer[checked_index++] = '\0';
                return LINE_OK;
            }
            //否则说明语法有问题.'\r'后边如果不是'\n',则说明语法有误。
            return LINE_BAD;
        } else if(temp == '\n'){
            //为什么是>1,第0个不能是'\r'? 因为http有头部,头部不能为空
            if(checked_index > 1 && buffer[checked_index - 1] == '\r') {
                buffer[checked_index - 1] = '\0';
                buffer[checked_index++ ] = '\0';
                return LINE_OK;
            }
            //否则说明语法有问题.'\n'前边如果不是'\r',则说明语法有误。
            return LINE_BAD;
        }
    }
    //如果分析完了也没有遇到'\r'字符,则说明还需要继续读取数据
    return LINE_OPEN;
}

//分析请求行,请求行分析完之后把CHECK_STATE转移到头部分析
//请求行结构:请求方法|'\t'或者空格|URL|'\t'或者空格|协议版本|\r\n|
HTTP_CODE parse_requestline(char *temp, CHECK_STATE& checkstate){
    //strptr(s1, s2)用于s1中查找有没有s2字符串中的某个字符,有的话返回指向字符的指针,没找到返回NULL
    //请求行肯定有'\t'换行符或者空格,如果没有则说明有问题
    char *point = strpbrk(temp, " \t");
    if(!point){
        //语法有问题
        return BAD_REQUEST;
    }
    //把'\t'字符替换成'\0'
    *point++ = '\0';

    //由于吧'\t'替换成了'\0',所以temp就是"GET"和"POST"
    char *method = temp;
    //strcasecmp(s1, s2)比对s1和s2字符串,相同返回0,s1大于s2返回大于0的值,s1小于s2返回小于0的值。
    if(strcasecmp(method, "GET") == 0) {
        printf("The request method is GET\n");
    } else {
        //只支持GET方法
        return BAD_REQUEST;
    }
    //size_t strspn(const char *str1, const char *str2) 检索字符串 str1 中第一个不在字符串str2中出现的字符,的下标。
    //这里相当于防止中间有多个\t,直接把point指到第一个不是空格或\t的地方
    point += strspn(point, "\t");
    //先跳过URL的分析,获取版本号
    char *version = strpbrk(point, " \t");
    if(!version){
        //语法有问题
        return BAD_REQUEST;
    }
    *version++ = '\0';
    //这里相当于防止中间有多个\t,直接把version指到第一个不是空格或\t的地方
    version += strspn(point, "\t");
    //只支持HTTP/1.1
    if(strcasecmp(version, "HTTP/1.1") != 0){
        return BAD_REQUEST;
    }

    //获取URL
    char *url = point;
    //检查URL是否合法,url是不是以http://起头
    if(strncasecmp(url, "http://", 7) == 0){
        //跳过开头http://
        url += 7;
        //char *strchr(char *s1,char c1):从字符串s1中寻找字符c1第一次出现的位置。找到返回指针,没找到返回NULL
        url = strchr(url, '/');
    }
    if(!url || url[0] != '/'){
        return BAD_REQUEST;
    }
    printf("The request URL id: %s\n", url);
    //HTTP请求行处理完毕,状态转移到头部字段的分析。
    checkstate = CHECK_STATE_HEADER;
    //因为只分析了请求行,还需要分析头部
    return NO_REQUEST;
}

//分析头部字段
/*头部字段结构:
头部字段名|:|值|\t
...
头部字段名|:|值|\t
\r\n
*/
HTTP_CODE parse_headers(char *temp){
    //如果第一个是\0说明已经把头部字段结构分析完成了
    if(temp[0] == '\0'){
        return GET_REQUEST;
    }
    //处理Host头部字段
    if(strncasecmp(temp, "Host:", 5) == 0){
        temp += 5;
        //跳过中间可能存在的空格和\t
        temp += strspn(temp, " \t");
        printf("The request host id %s\n", temp);
    } else { //其他头部字段暂时不处理
        printf("Can not handle this header.\n");
    }
    //继续分析下一个头部
    return NO_REQUEST;
}


//分析HTTP请求的入口函数
HTTP_CODE parse_content(char *buffer, int &check_index, CHECK_STATE &checkstate, int &read_index, int &start_line){
    //当前行的读取状态
    LINE_STATUS linestate = LINE_OK;
    //记录HTTP请求处理结果
    HTTP_CODE retcode = NO_REQUEST;
    
    //主状态机,从buffer中取出所有完整的行
    while((linestate = parse_line(buffer, check_index, read_index)) == LINE_OK) {
        //start_line是行在buffer中的起始位置,因为有可能不是从buffer的起始位置开始分析,但是这样上边的parse_line为什么不加上?check_index的意思不就是之前的字节已经分析好了。
        char *temp = buffer + start_line;
        start_line = check_index;
        //选择分析请求行还是头部
        switch(checkstate){
            //分析请求行
            case CHECK_STATE_REQUESTLINE:
            {
                retcode = parse_requestline(temp, checkstate);
                //分析请求行失败
                if(retcode == BAD_REQUEST){
                    return BAD_REQUEST;
                }
                break;
            }
            case CHECK_STATE_HEADER://分析头部
            {
                retcode = parse_headers(temp);
                //获取到了GET请求
                if(retcode == GET_REQUEST){
                    return GET_REQUEST;
                }
                break;
            }
            default://不支持其他
            {
                return INTERNAL_ERROR;
                break;
            }
        }
    }
    //没有读取到完整的行,需要继续读取客户数据
    if(linestate == LINE_OPEN){
        return NO_REQUEST;
    } else {
        return BAD_REQUEST;
    }
}

int main(int argc, char* argv[]){
    if(argc <= 2){
        printf("Ussage: %s ip_address port_number.\n", basename(argv[0]));
    }
    createSockfd sockfd(argv[1], atoi(argv[2]));
    sockfd.bindSockfd();
    sockfd.listenfd();
    sockfd.acceptfd();

    if(sockfd.connfd < 0){
        printf("Accept error, errno is: %d.\n", errno);
    } else {
        char buffer[BUFFER_SIZE];
        memset(buffer, '\0', BUFFER_SIZE);
        int data_read = 0;
        int read_index = 0;     //当前已经读取了多少字节用户数据
        int checked_index = 0;  //当前已经分析了多少字节用户数据
        int start_line = 0;     //行在buffer中的起始位置
        CHECK_STATE checkstate = CHECK_STATE_REQUESTLINE;
        //循环读取客户请求
        while(1){
            data_read = recv(sockfd.connfd, buffer + read_index, BUFFER_SIZE - read_index, 0);
            //读取失败
            if (data_read == -1){
                printf("Reading failed\n");
                break;
            } else if(data_read == 0){  //关闭了socket连接
                printf("Remote client has close the connection.\n");
                break;
            }
            read_index += data_read;
            //分析目前已经获取道德所有客户数据
            HTTP_CODE result = parse_content(buffer, checked_index, checkstate, read_index, start_line);
            //还没完整读到一个HTTP请求
            if(result == NO_REQUEST){
                continue;
            }
            //获得了一个GET请求
            else if(result == GET_REQUEST){
                send(sockfd.connfd, szret[0], strlen(szret[0]), 0);
                break;
            }
            //其他情况代表错误发生
            else{
                send(sockfd.connfd, szret[1], strlen(szret[1]), 0);
                break;
            }
        }
    }

    return 0;
}

8.7 提高服务器性能的其他建议

8.7.1 池

  • 提高服务器性能的一个直接方法,就是以空间换事件。
  • 池:一组资源的集合,在服务器启动之初就被创建并初始化,也就是静态资源分配。当开始处理请求时,如果需要相关资源,直接从池中获取,无需动态分配。
  • 因为分配系统资源你的系统用调用很耗时,当服务器处理完请求,可以把资源放回池里,无需动态进行资源的获取和释放,减少服务器对内核的频繁访问。
  • 常见的池有:内存池、进程池和连接池。

8.7.2 数据复制

  • 高性能服务器应该避免不必要的数据复制。
  • 如果内核可以直接处理从socket或者文件中读入数据,程序就没必要将这些数据从内核缓冲区复制到应用程序缓冲区。
  • 例如ftp,不需要直到资源具体内容,不需要复制到缓存再send发送,而是可以直接sendfile来直接发送到客户端。

8.7.3 上下文切换和锁

  • 上下文切换问题:进程切换或线程切换导致的系统开销问题。
  • 为一个客户连接创建一个工作线程是不可取的,一个线程应该允许处理多个客户连接。
  • 如果可以,避免锁的使用。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一个进程池的服务器程序 下面做了非常简单的http服务器,该服务器只能接收Get请求。 流程大概如下: 1,父进程listen,创建pipe(下面所有父子进程之间的通信都用该pipe) 2,父进程预fork n个子进程 3,各个子进程accept(listenfd),即所有子进程竞争accept请求。由于listenfd是在fork之前就有的,所以所有子进程都可以访问到,不需用到“进程间文件描述符传递”问题; 4,子进程每accept到一个请求都告诉父进程,父进程把请求数加1;子进程没完成一个请求,父进程把请求数减1;当父进程发现请求数 >= 子进程数时,父进程创建新的子进程,并把子进程数加1(当然子进程数有个预先上限);当父进程发现子进程数大于请求数加1时,父进程杀死多余的子进程。 总的来说,思想是让子进程accept并处理请求,父进程通过子进程发来的信息控制请求数与子进程数之间的关系。 代码如下: 代码如下: #include <time.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <dirent.h> #include <sys/stat.h> #include <signal.h> #include <sys/wait.h> #include <pthread.h> #include <errno.h> #include <unistd.h> #include <fcntl.h> #define PRECHILD 5 #define MAXCHILD 50 #define BUFSIZE 4096 #define PIDPATH "pid" #define head503 "HTTP/1.1 503 Service unavailable\r\n" #define head404 "HTTP/1.1 404 Not Found\r\n" #define head200 "HTTP/1.1 200 0K\n\rContent—Type: text/html\n\rContent—Length: " int len503, len404, len200; int fd1[2], fd2[2]; typedef struct { pid_t pid; char status; // 'n' means new request; 'f' means finish the request } REPORT; void answer(int listenfd) { int connfd; char buf[BUFSIZE]; int count; int pid = getpid(); struct sockaddr_in cliaddr; int size = sizeof(cliaddr); char comm; REPORT rep; rep.pid = pid; while (1) { connfd = accept(listenfd, (struct sockaddr *)&cliaddr,(socklen_t *)&size ); //子进程accept请求 rep.status = 'n'; if (write(fd1[1], &rep, sizeof(rep)) < 0) { //通知父进程已经accept了请求 perror("write pipe new failed"); exit(-1); } count = read(connfd, buf, BUFSIZE); char req[10]; char filepath[256]; sscanf(buf, "%s%s", req, filepath + 1); filepath[0] = '.'; if (strcmp("GET", req) != 0) {//503 write(connfd, head503, len503); //goto err_out; close(connfd); exit(-1); } char content[BUFSIZE]; struct stat stbuf; if (lstat(filepath, &stbuf) != 0) { int err = errno; if (err == ENOENT) {//404 write(connfd, head404, len404); } close(connfd); exit(-1); } count = write(connfd, head200, len200); u_int filesize = stbuf.st_size; sprintf(content, "%u\n\r\n\r", filesize); count = write(connfd, content, strlen(content)); FILE *fp = fopen(filepath, "r"); if (fp == NULL) { printf("open file %s failed\n", filepath); close(connfd); exit(-1); } while((count = fread(content, 1, sizeof(content), fp)) > 0) { //printf("%s", content); if (write(connfd, content, count) != count) { printf("write failed\n"); } } fclose(fp); close(connfd); rep.status = 'f'; if (write(fd1[1], &rep, sizeof(rep)) < 0) {//告诉父进程自己处理完了请求 perror("write pipe finish failed"); exit(-1); } if (read(fd2[0], &comm, 1) < 1) {//等待来自父进程的命令 perror("read pipe failed"); exit(-1); } //printf("[%d] reve %c from pa\n", pid, comm); if (comm == 'e') { //收到exit命令 printf("[%d] exit\n", pid); exit(-1); } else if (comm == 'c') { //收到继续accept的命令 printf("[%d] continue\n", pid); } else { printf("[%d] comm : %c illeagle\n", pid, comm); } } } void usage() { printf("Usage: http-serv port\n"); } int write_pid() { int fd; if ((fd = open(PIDPATH, O_WRONLY | O_TRUNC | O_CREAT, S_IWUSR)) < 0){ perror("open pidfile faild"); return -1; } struct flock lock; lock.l_type = F_WRLCK; lock.l_start = 0; lock.l_whence = SEEK_SET; lock.l_len = 0; if (fcntl(fd, F_SETLK, &lock) == -1) { int err = errno; perror("fcntl faild"); if (err == EAGAIN) { printf("Another http-serv process is running now!\n"); } return -1; } return 0; } void daemon_init() { //clear file creation mask; umask(0); //become a session leader if (fork() != 0) exit(-1); if (setsid() < 0) exit(-1); //make sure can be never get the TTY control if (fork() != 0) exit(-1); //may chdir here int i; for (i = 0; i < 1024; i++) close(i); /* * Attach file descriptors 0, 1, and 2 to /dev/null. */ int fd0, fd1, fd2; fd0 = open("/dev/null", O_RDWR); fd1 = dup(0); fd2 = dup(0); if (fd0 != 0 || fd1 != 1 || fd2 != 2) { printf("init failed\n"); exit(-1); } } int main(int argc, char **argv) { int listenfd; struct sockaddr_in servaddr; pid_t pid; if (argc != 2) { usage(); return -1; } signal(SIGCHLD, SIG_IGN); len200 = strlen(head200); len404 = strlen(head404); len503 = strlen(head503); daemon_init(); //转为后台程序,如需打印调试,把这行注释掉 if (write_pid() < 0) //避免同时有多个该程序在运行 return -1; if (pipe(fd1) < 0) { perror("pipe failed"); exit(-1); } if (s_pipe(fd2) < 0) { perror("pipe failed"); exit(-1); } int port = atoi(argv[1]); //initialize servaddr and listenfd... bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port); listenfd = socket(AF_INET, SOCK_STREAM, 0); bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); listen(listenfd, 1000); int i; for (i = 0; i < PRECHILD ; i++) { //父进程预fork 子进程 if ((pid = fork()) < 0) { perror("fork faild"); exit(3); } else if (pid == 0) { answer(listenfd); } else { printf("have create child %d\n", pid); } } char e = 'e'; char c = 'c'; int req_num = 0; int child_num = PRECHILD; REPORT rep; while (1) { //printf("req_num = %d, child_num = %d\n", req_num, child_num); if (read(fd1[0], &rep, sizeof(rep)) < sizeof(rep)) {//等待子进程发来消息 perror("parent read pipe failed"); exit(-1); } //printf("parent: receive from %d\n", pid); if (rep.status == 'n') {//子进程刚accept了新的请求 req_num ++; printf("parent: %d have receive new request\n", rep.pid); if (req_num >= child_num && child_num <= MAXCHILD) { //请求数过多,创建更多子进程 if ((pid = fork()) < 0) { perror("fork faild"); exit(3); } else if (pid == 0) { answer(listenfd); } else { printf("have create child %d\n", pid); child_num ++; } } } else if (rep.status == 'f') {//子进程刚处理完了一个请求 req_num --; //printf("parent: %d have finish a request\n", rep.pid); if (child_num > (req_num + 1) && child_num > PRECHILD) {//子进程数过多,删除多余的子进程 if (write(fd2[1], &e, sizeof(e)) < sizeof(e)) { perror("pa write pipe failed"); exit(-2); } //printf("tell child exit\n"); child_num --; } else { if (write(fd2[1], &c, sizeof(c)) < sizeof(c)) {//让子进程继续等待accept perror("pa write pipe failed"); exit(-2); } //printf("tell child continue\n"); } } } return 0; } 利用fork()创建多个子进程 11:09 pm on Oct 23rd 2010 greenMay 之间我学习了创建一个子进程,也大致理解了子进程与父进程的关系。今天无意间遇到一个创建多个子进程的问题,结果还发现了点小bug,现在写下来和大家分享。 我需要实现的目标如下:编写一段源程序,使系统调用fork()创建两个子进程,当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程在屏幕上显示一个字符:父进程显示字符“a”;子进程分别显示字符“b”和字符“c”。 一开始我的主要代码如下: view source print? 01 int main() 02 { 03 pid_t child1; 04 pid_t child2; 05 child1 = fork(); 06 child2 = fork(); 07 if(child1 == 0) 08 { 09 printf("Child1:a\n"); 10 return 0; 11 } 12 if(child2 == 0) 13 { 14 printf("Child2:b\n"); 15 return 0; 16 } 17 else 18 { 19 waitpid(child1,NULL,0); 20 waitpid(child2,NULL,0); 21 printf("Parent:c\n"); 22 } 23 return 0; 24 } 奇怪的是,我得到的是这样一个结果: Child1:a Child1:a Child2:b Parent:c 竟然有两个Child1。可是我的代码里明明只是让Chidl1打印一次啊。搜索到一篇好的博文。文章仔细分析了和我几乎相同的情况。事实上,是我的粗心和对fork()的理解不深刻导致了上述的奇怪问题。 我们知道,fork()之后,我们还是首先执行的是父进程,也就是如下代码段: view source print? 1 waitpid(child1,NULL,0); 2 waitpid(child2,NULL,0); 3 printf("Parent:c\n"); 然后waitpid(child1,NULL,0),进入child1的执行。child1将要执行的是如下的的代码段: view source print? 1 child2 = fork(); 2 if(child1 == 0) 3 { 4 printf("Child1:a\n"); 5 return 0; 6 } 注意,第一行的那个child2 = fork()!这就意味着对于child1来说,它自己又要创建一个子进程,这时候他成为了父亲。这时候,它有一个儿子child2,但是这个child2不同与我们刚才定义的那个child2,这个child2其实是parent的孙子。之所以又打印了一边Child1。如果加上如下代码就明白了: view source print? 01 child2 = fork(); 02 if(child1 == 0) 03 { 04 if(child2 == 0) 05 { 06 printf("GrandChild!\n"); 07 } 08 printf("Child1:a\n"); 09 return 0; 10 } 这时候将出现: Child1:a GrandChild! Child1:a Child2:b Parent:c 恩,这就很明白了!我无意间多调用了一次child2=fork(); 所以,如果要达到我最初的目的,需要改变child2的fork()的位置: view source print? 01 #include <stdio.h> 02 #include <unistd.h> 03 #include <sys/types.h> 04 #include <sys/wait.h> 05 int main() 06 { 07 pid_t child1; 08 pid_t child2; 09 child1 = fork(); 10 child2 = fork(); 11 if(child1 == 0) 12 { 13 printf("Child1:a\n"); 14 return 0; 15 } 16 if(child2 == 0) 17 { 18 printf("Child2:b\n"); 19 return 0; 20 } 21 else 22 { 23 waitpid(child1,NULL,0); 24 waitpid(child2,NULL,0); 25 printf("Parent:c\n"); 26 } 27 return 0; 28 } 我参照的那个博文最后给出了一个更为普遍的fork()创建多进程的程序框架: view source print? 01 pid_t create_child() 02 { 03 pid_t p = fork(); 04 if( p == 0 ) 05 { 06 printf("in child %d\n", getpid()); 07 //do something 08 return 0; 09 } 10 return p; 11 } 12 int main(void) 13 { 14 pid_t p1 = create_child(); 15 pid_t p2 = create_child(); 16 17 int st1, st2; 18 waitpid( p1, &st1, 0); 19 waitpid( p2, &st2, 0); 20 printf("in parent, pid = %d\n", getpid()); 21 printf("in parent, child 1 exited with %d\n", st1); 22 printf("in parent, child 2 exited with %d\n", st2); 23 return 0; 24 } 注意到,期中的create_child()函数最后有一个return p。这个return p将pid返回给了父进程,其实也是将子进程对于CPU的控制权交还给了父进程,这样就避免了多个子进程在创建之时互相影响了。 可以说,今天的这个问题真是一个有趣的事情。代码有的时候就是这么奇怪~ 最后,向我引用的那篇文章致敬! Linux内核对多进程和多线程的支持方式: 线程机制支持并发程序设计技术,在多处理器上能真正保证并行处理。而在linux实现线程很特别,linux把所有的线程都当作进程实现。linux下线程看起来就像普通进程(只是该进程和其他进程共享资源,如地址空间)。上述机制与Microsoft windows或是Sun Solaris实现差异很大。 Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用__clone()和fork(),最终都用不同的参数调用do_fork()核内API。 do_fork() 提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当使用fork系统调用产生多进程时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境。当使用pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的”进程”拥有共享的运行环境,只有栈是独立的,由 __clone()传入。 即:Linux下不管是多线程编程还是多进程编程,最终都是用do_fork实现的多进程编程,只是进程创建时的参数不同,从而导致有不同的共享环境。Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。pthread 库使用一个管理线程(__pthread_manager() ,每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号,而主线程pthread_create()) 的调用者则通过管道将请求信息传给管理线程。 很多朋友都说使用多线程的好处是资源占用少,其隐含之意就是说进程占用资源比线程多,对吧?但实际上Linux下多进程是否就真的点用很多资源呢?暂且不说进程是否比线程占用资源多,就进程占用资源的多少情况而言,Linux确实是做得相当节省的。产生一个多进程时肯定是要产生的一点内存是要复制进程表项,即一个task_struct结构,但这个结构本身做得相当小巧。其它对于一个进程来说必须有的数据段、代码段、堆栈段是不是全盘复制呢?对于多进程来说,代码段是肯定不用复制的,因为父进程和各子进程的代码段是相同的,数据段和堆栈段呢?也不一定,因为在Linux里广泛使用的一个技术叫copy-on-write,即写时拷贝。copy-on-write意味着什么呢?意味着资源节省,假设有一个变量x在父进程里存在,当这个父进程创建一个子进程或多个子进程时这个变量x是否复制到了子进程的内存空间呢?不会的,子进程和父进程使用同一个内存空间的变量,但当子进程或父进程要改变变量x的值时就会复制该变量,从而导致父子进程里的变量值不同。父子进程变量是互不影响的,由于父子进程地址空间是完全隔开的,变量的地址可以是完全相同的。 Linux的”线程”和”进程”实际上处于一个调度层次,共享一个进程标识符空间,这种限制使得不可能在Linux上实现完全意义上的POSIX线程机制,因此众多的Linux线程库实现尝试都只能尽可能实现POSIX的绝大部分语义,并在功能上尽可能逼近。Linux进程的创建是非常迅速的。内核设计与实现一书中甚至指出Linux创建进程的速度和其他针对线程优化的操作系统(Windows,Solaris)创建线程的速度相比,测试结果非常的好,也就是说创建速度很快。由于异步信号是内核以进程为单位分发的,而LinuxThreads的每个线程对内核来说都是一个进程,且没有实现”线程组”,因此,某些语义不符合POSIX标准,比如没有实现向进程中所有线程发送信号,README对此作了说明。LinuxThreads中的线程同步很大程度上是建立在信号基础上的,这种通过内核复杂的信号处理机制的同步方式,效率一直是个问题。LinuxThreads 的问题,特别是兼容性上的问题,严重阻碍了Linux上的跨平台应用(如Apache)采用多线程设计,从而使得Linux上的线程应用一直保持在比较低的水平。在Linux社区中,已经有很多人在为改进线程性能而努力,其中既包括用户级线程库,也包括核心级和用户级配合改进的线程库。目前最为人看好的有两个项目,一个是RedHat公司牵头研发的NPTL(Native Posix Thread Library),另一个则是IBM投资开发的NGPT(Next Generation Posix Threading),二者都是围绕完全兼容POSIX 1003.1c,同时在核内和核外做工作以而实现多对多线程模型。这两种模型都在一定程度上弥补了LinuxThreads的缺点,且都是重起炉灶全新设计的。 综上所述的结论是在Linux编程多用多进程编程少用多线程编程。 IBM有个家伙做了个测试,发现切换线程context的时候,windows比linux快一倍多。进出最快的锁(windows2k的 critical section和linux的pthread_mutex),windows比linux的要快五倍左右。当然这并不是说linux不好,而且在经过实际编程之后,综合来看我觉得linux更适合做high performance server,不过在多线程这个具体的领域内,linux还是稍逊windows一点。这应该是情有可原的,毕竟unix家族都是从多进程过来的,而 windows从头就是多线程的。 如果是UNIX/linux环境,采用多线程没必要。 多线程比多进程性能高?误导! 应该说,多线程比多进程成本低,但性能更低。 在UNIX环境,多进程调度开销比多线程调度开销,没有显著区别,就是说,UNIX进程调度效率是很高的。内存消耗方面,二者只差全局数据区,现在内存都很便宜,服务器内存动辄若干G,根本不是问题。 多进程是立体交通系统,虽然造价高,上坡下坡多耗点油,但是不堵车。 多线程是平面交通系统,造价低,但红绿灯太多,老堵车。 我们现在都开跑车,油(主频)有的是,不怕上坡下坡,就怕堵车。 高性能交易服务器中间件,如TUXEDO,都是主张多进程的。实际测试表明,TUXEDO性能和并发效率是非常高的。TUXEDO是贝尔实验室的,与UNIX同宗,应该是对UNIX理解最为深刻的,他们的意见应该具有很大的参考意义 1. 散沙 2010年7月10日08:43 回复 | 引用 | #1 文章很有深度,我们把握一个尺度就可以了,在windows下使用线程,unix下则使用进程就可以了 2. rjoo 2010年9月9日13:49 回复 | 引用 | #2 错的太多了,博主,应该看看新资料了。 现在都2010年了,NPTL早就取代了老的Linux thread。而且通常多线程有性能优势,但是多进程更稳定,并且通常性能瓶颈不在于是进程模型还是线程模型而在于IO。 3. rjoo 2010年9月9日13:56 回复 | 引用 | #3 关于那个critical section和pthread_mutex_t,critical section本质上是一个自旋锁,短期锁当然快,不知道你说的那个IBM的哥们怎么比的,要比也该是和pthread_spinlock_t比。 4. admin 2010年9月9日17:28 回复 | 引用 | #4 rjoo挺热心的,呵呵,这篇文章不是我写的,但有几个地方我可以解答一下: 1. Linux下没有线程的概念,pthread线程实质是通过轻量级进程实现的。你说瓶颈在IO,这一点我很赞同你的意见,作者如果能再写个IO操作的文章来的话就会更好了。 2. mutex和critical section的确是不能比的。一个涉及到内核,一个没有涉及到内核。呵呵,很佩服你对这些东西的掌握程度,有机会多交流。 ^_^ 5. 定时 2010年9月9日17:40 回复 | 引用 | #5 我们组的最近项目的经验告诉我们能用多进程不用多线程,多线程安全编程难,而且锁会早成效率很低,甚至不如单线程,你说的NPTL我知道,他只是多线程优化了并不能改变多线程安全编程的问题,锁的问题。谢谢指教,实践出真知。 @rjoo 6. 定时 2010年9月9日17:44 回复 | 引用 | #6 你说的锁,我确实不太了解,但是我们leader对它很了解,就是最近的一个项目,锁搞得他很郁闷,他也终于同意我的关键,尽可能不用多线程。 @rjoo 7. rjoo 2010年9月29日13:41 回复 | 引用 | #7 @admin Linux下没有线程的概念,pthread线程实质是通过轻量级进程实现的—这是2.4内核以前的情况(实际上是2.0时引入的,那可实在是太久了),2.4内核引入NGPL,2.6内核线程支持改为NPTL。NPTL实现的是1:1的线程模型(有资料说Win也是这种实现,虽然不太确定,但我觉得可能性很大),而NGPT虽然是理论上最先进的m:n线程模型,但最后实现出来的性能差NPTL一大截,最后被抛弃。看看文中说法就知道要么文章写的很早,要么作者看了一堆十年前的资料。 给个链接: http://www.kegel.com/c10k.html#threads.linuxthreads 8. finalday 2010年10月15日17:26 回复 | 引用 | #8 忍不住跳出来说,作者对并发编程的理解还不行。 比如说锁的问题,说得好像是多线程才需要的东西一样。如果一个应用多进程时完全不用锁,多线程也就多一个轻量级锁——锁一下,各回各家,每个线程用自己的专有存储,之后不就和多进程一样了?这样会被搞得很郁闷?当然不会。所以说明那个应用对于数据共享的需求不是这么简单,既然不是这么简单,多进程程序一样要加锁。多进程的加解锁代价可比多线程大得多了,共享数据和协作也麻烦多了。 多线程编程难不难?难,但这是由于并发本身的难度引起的。“锁”,“安全编程”不管是多线程还是多进程都一样会遇到。 多线程的最大优点是数据共享和协作方便。 多进程的最大优点是挂了一个进程不会影响其他进程,资源也不会泄露,故比较能容忍程序员犯错。 至于两者裸奔比性能,真的没啥意义。
第一章 绪论 1.1. 选题背景 这几年,许多新名词涌入我们的视野:博客、圈子、播客、WAP等。这些都预示着我们进入了一个新的互联网阶段web 2.0,它是相对web 1.0的新的一类互联网应用的总称,是一次从核心内容到外部应用的革命[10]。这个阶段发展迅速,互联网应用趋于多样化,其中变化最大的是由web 1.0网站创造内容的时代变为由用户创造内容的web 2.0时代。 在web 2.0应用中,博客(Blog)是web 2.0核心应用中最典型、最流行的代表之一,也是web 2.0技术应用的最直观的表现,是web 2.0精神和理念的具体体现。 1.2. 问题的提出 Blog记载了日常发生的事情和自己的兴趣爱好,把自己的思想和知识和他人分享、交流,同时又通过“六度空间”结识了更多志趣相投的朋友;而越来越多专业知识的 Blog 的出现,让我们看到了 Blog 更多所蕴涵的巨大的信息价值:不同的 Blog 选择不同的内容,收集和整理成为很多人关注的专业 Blog ——目前越来越多的人获取信息的来源是一些固定的 Blog 。随着博客人数的增加, Blog 作为一种新的生活方式、新的工作方式、新的学习方式已经被越来越多的人所接受,并且在改变传统的网络和社会结构:网络信息不再是虚假不可验证的,交流和沟通更有明确的选择和方向性,单一的思想和群体的智慧结合变的更加有效,个人出版变成人人都可以实现的梦想—— Blog 正在影响和改变着我们的生活。 1.3. 系统的开发目标 管理员通过前台页面进入后台管理模块后,可对注册的博客用户进行维护,包括对注册用户的添加、查找、修改和删除。 管理员进入登录后,可对帐户进行管理,包括添加管理员帐户、修改管理员帐户、删除管理员帐户和对帐户进行权限设置。 博客用户通过前台登录后,可对自己的博客空间进行管理,包括发布自己的网络日志、收藏个人图片、和相关人员进行交流和沟通以及删除访问者发表的评论等。 因此,在具体设计实现该博客网站时,主要考虑了主流博客网站的几个主要功能:(1)博客的注册、登录验证功能(2) 网络用户通过关键字搜索博文功能(3) 最热门博客页面推荐浏览(4) 文章详细内容及相关评论显示(5) 博客页面访问量统计(6) 博客个人文章管理维护功能(7) 博客个人文章分类管理维护功能(8) 博客个人友情链接维护功能(9) 博客个人基本信息管理维护功能(10) 博客图片上传及个人相册管理(11) 网络用户写留言,博主查看留言。 第二章 系统设计 2.1. 系统分析 在整个blog进行开发之前,要确定出整个项目的整体架构,包括系统的选型、运行环境的确定及系统结构设计。下面对这进行详细介绍。 在进行软件系统开发的最初环节,一般都需要进行系统的选型,即根据系统功能的实际需求,选择合适的开发工具及软件架构。 blog对系统的可靠性、稳定性有比较高的要求。本系统设计时,比较主流的B/S设计有基于JSP、ASP、PHP、CGI及J2EE等模式。相比较而言PHP的功能相对简单,不适合做大程序;而CGI效率相对较低,所以也不考虑。由于J2EE的开源的框架中提供了MVC模式实现框架Struts、对象关系模型中的Hibernate 的框架及拥有事务管理和依赖注入的Spring。利用现存框架可以更快开发系统。所以选择Java技术作为blog 的开发工具。 为了增加系统的吞吐量,提高并发处理客户请求数量,系统采用了IBM服务器作为主机。在数据库处理方面,不需要在数据层借助存储过程及数据库服务器端函数封装过多的业务逻辑,因此数据库系统采用相对精巧的MySQL[6]。 该在线博客系统服务器端如果需要布置到其他主机上,则该主机必备条件如下: 1. 服务器端操作系统:独立于操作系统的跨平台系统,客户端MicroSoft Windows 2000及以上; 2. 数据库:MySQL 5.0.27版本; 3. Web服务器:Tomcat 5.5及以上版本,配合MVC设计模式及 Hibernate开发架构; 4. 客户端运行环境:能运行IE 5以上或Netscape 5以上浏览器的操作系统,配合使用Ajax技术; 5. 客户端运行工具:目前的系统采用浏览器作为客户端,为了支持Ajax开发框架,应该选择使用IE 5以上版本浏览器。 本网站以xp为Web平台,JSP+Ajax+Servlet+JavaBean+Hibernate为网站实现技术,建立基于MySQL数据库系统的核心动态网页,实现博客网站前台及博客个人维护管理等功能模块。 1、 系统处理的准确性和及时性:系统处理的准确性和及时性是系统的必要性能。在系统设计和开发过程中,要充分考虑系统当前和将来可能承受的工作量,使系统的处理能力和响应时间能够满足信息处理的需求。 2、 系统的开放性和系统的可扩充性:系统在开发过程中,应该充分考虑以后的可扩充性。例如数据表中用户选择字段方式的改变,用户查询的需求也会不断的更新和完善。所有这些,都要求系统提供足够的手段进行功能的调整和扩充。而要实现这一点,应通过系统的开放性来完成,既系统应是一个开放系统,只要符合一定的规范,可以简单的加入和减少系统的模块,配置系统的硬件。通过软件的修补、替换完成系统的升级和更新换代。 3、 系统的易用性和易维护性:要实现这一点,就要求系统应该尽量使用用户熟悉的术语和中文信息的界面;针对用户可能出现的使用问题,要提供足够的在线帮助,缩短用户对系统熟悉的过程。 4、 系统的数据要求:1、数据录入和处理的准确性和实时性。2、数据的一致性与完整性。3、数据的共享与独立性。 2.2. 系统的可行性分析 2.2.1. 技术可行性 技术上的可行性分析要考虑将来要采用的硬件和软件技术能否满足用户(这里是服务器,网速)提出的要求(如计算机的容量、速度等)。此外,还要考虑开发人员的水平,学习了两年的jsp开发,对于这个系统的编写,我想完整的之需要两个月就可以写出程序,再花上几天的调试,计划两个月左右就可以完成投入使用了。 我们掌握了数据库及其应用技术、数据库原理、计算机网络技术等课程,对数据库的设计、应用、维护及局域网的组成有了深刻的认识与一定的动手实践能力,考取了信息处理、程序设计、数据库技术等国家IT认证。从一定程度上具备了开发一个小型系统的能力。再有就! 2.2.2. 经济可行性 主要从对项目的经济上进行分析评价,一方面是支出的费用,包括设备购置费、管理和维护费用、人员工资和培训费等,另一个是取得的收益。这是个小型的系统,从投入的人力,财力与物力来讲是非常小的,发布出去之需要注册域名就可以了,从节省人力方面,可以让管理人员从繁与复杂的工作中解脱出来,做更多的工作。 2.2.3. 管理可行性 有IP地址、用户名与密码等,可以下载一个FTP上传工具(cure)上传更新后的内容.可以在http://www.skycn.com/soft/683.html下载到. 下载以后,通过"站点管理"-->"新建站点".输入IP,用户名,密码.然后连接即可. 当然也可用FTP工具.直接在IE浏览器地址栏输入ftp://www.域名/然后输入用户名和密码.同样可以上传.随时更新文件! 第三章 开发环境的说明与安装 3.1. 开发语言的选择 3.1.1. JAVA简介 Java是Sun公司推出的新的一代面向对象程序设计语言,特别适合于Internet应用程序开发。Java的产生与流行是当今Internet发展的客观要求,Java是一门各方面性能都很好的编程语言,它的基本特点是简单、面向对象、分布式、解释的、健壮的、安全的、结构中立的、可移植的、性能很优异的、多线程的、动态的,特别适合在Internet环境上开发的应用系统。 3.1.2. Web应用程序开发环境—JSP技术 JSP的全称是Java Server Pages,它是SUN推出的一种动态网页技术标准。它在传统的静态页面文件(*.html,*.htm)中加入JAVA程序片段和JSP标记,就构成了JSP页面。JSP具有以下的优点: 1、将业务层与表示层分离:使用JSP技术,网络开发人员可充分使用HTML来设计页面显示部分(如字体颜色等),并使用JSP指令或者JAVA程序片段来生成网页上的动态内容; 2、能够跨平台:JSP支持绝大部分平台,包括现在非常流行的LINUX系统,应用非常广泛的Apache服务器也提供了支持JSP的服务; 3、组件的开发和使用很方便:如ASP的组件是由C++,VB等语言开发的,并需要注册才能使用;而JSP的组件是用Java开发的,可以直接使用; 4、一次编写,处处运行:作为JAVA开发平台的一部分,JSP具有JAVA的所有优点,包括Write once , Run everywhere. 3.2. 数据库的选择 3.2.1. Web应用程序开发环境—SQLserver数据库 SQL Server是由Microsoft开发和推广的关系数据库管理系统(DBMS),它最初是由Microsoft、Sybase和Ashton-Tate三家公司共同开发的。SQL Server 2000是Microsoft公司于2000年推出的最新版本。 SQL Server 特点:   1.真正的客户机/服务器体系结构。   2.图形化用户界面,使系统管理和数据库管理更加直观、简单。   3.丰富的编程接口工具,为用户进行程序设计提供了更大的选择余地。   4.SQL Server与Windows NT完全集成,利用了NT的许多功能,如发送和接受消息,管理登录安全性等。SQL Server也可以很好地与Microsoft BackOffice产品集成。   5.具有很好的伸缩性,可跨越从运行Windows 95/98的膝上型电脑到运行Windows 2000的大型多处理器等多种平台使用。   6.对Web技术的支持,使用户能够很容易地将数据库中的数据发布到Web页面上。   7.SQL Server提供数据仓库功能,这个功能只在Oracle和其他更昂贵的DBMS中才有。   SQL Server 2000与以前版本相比较,又具有以下新特性 :   1.支持XML(Extensive Markup Language,扩展标记语言)   2.强大的基于Web的分析   3.支持OLE DB和多种查询   4.支持分布式的分区视图   安装、运行SQL Server 2000的硬件需求   (1)计算机   Inter及其兼容计算机,Pentium 166Mz或者更高处理器或DEC Alpha和其兼容系统。   (2)内存(RAM)   企业版最少64MB内存,其他版本最少需要32MB内存,建议使用更多的内存。   (3)硬盘空间   完全安装(Full)需要180MB的空间,典型安装(Typical)需要170MB的空间,最小安装(Minimum)需要65MB的空间。 3.3. 开发工具的选择 MyEclipse,Deamweare,选择SQLserver作为后台的数据库,选择JAVA、JSP、JavaScript、Html作为应用程序开发工具,运用Tomcat服务器技术,整个系统完全基于B/S (Browser/Server)模式进行设计。 1、Tomcat应用服务器 目前支持JSP的应用服务器是较多的,Tomcat是其中较为流行的一个Web服务器,被JavaWorld杂志的编辑选为2001年度最具创新的Java产品,可见其在业界的地位。 Tomcat是一个免费的开源的Serlvet容器,在Tomcat中,应用程序的部署很简单,你只需将你的WAR放到Tomcat的webapp目录下,Tomcat会自动检测到这个文件,并将其解压。你在浏览器中访问这个应用的Jsp时,通常第一次会很慢,因为Tomcat要将Jsp转化为Servlet文件,然后编译。编译以后,访问将会很快。 Tomcat也具有传统的Web服务器的功能:处理Html页面。但是与Apache相比,它的处理静态Html的能力就不如Apache。我们可以将Tomcat和Apache集成到一块,让Apache处理静态Html,而Tomcat处理Jsp和Servlet。Tomcat是一个很好的工具,不仅仅因为其免费,功能强大,更因为其开放性,越来越受到人们的重视。 2、 B/S 开发模式 伴随着Internet的迅速发展,计算机技术正在由基于C/S(client/ Server)模式的应用系统转变为基于B/S模式的应用系统。 过去,网络软件的开发都采用C/S(client)模式,在这种模式下,主要的业务逻辑都集中于客户端程序,因此,必然导致以下问题: 系统安装、调试、维护和升级困难。由于客户端的硬件配置可能存在差异,软件环能各不相同,因此,在安装时,必须对每一个客户端分别进行配置,同样,在软件升级时也要对客户端分别处理。 B/S模式带来了巨大的好处: 开发成本及维护成本降低。由于B/S架构管理软件只安装在服务器端(Server)上,即应用程序在部署、升级、维护时,只需要在服务器端进行配置就可以了,网络管理人员只需要管理服务器就行了,用户界面主要事务逻辑在服务器(Server)端完全通过WWW浏览器实现,极少部分事务逻辑在前端(Browser)实现,所有的客户端只有浏览器,网络管理人员只需要做硬件维护。 良好的安全性能,防火墙技术可以保证后台数据库的安全性。所有的配置工作都集中在服务器端且所有客户端请求都是通过DBMS来访问数据库,从而大大减少了数据直接暴露的风险。 第四章 系统设计 4.1开发框架技术介绍 对于框架技术,我们采用Struts+ajax的整合! Struts是应用比较广泛的一种表现层框架 1、 Struts是一个为开发基于模型(Model)-视图(View)-控制器(Controller)(MVC)模式的应用架构的开源框架,是利用Servlet,JSP和custom tag library构建Web应用的一项非常有用的技术。由于Struts能充分满足应用开发的需求,简单易用,敏捷迅速。它使用服务层框架可以将JavaBeans从Jsp/Servlet中分离出来,而使用表现层框架则可以将Jsp中剩余的JavaBeans完全分离,这部分JavaBeans主要负责显示相关信息,一般是通过标签库(Taglib)实现,不同框架有不同自己的标签库,Struts是应用比较广泛的一种表现层框架。 2、 Ajax我们主要应用就是xmlhttprequest,回调函数实现局部刷新达道数据更新! 4.2需求分析 Blog网站主要是实现注册用户登录、管理相关信息、博文及相关评论、查看留言、友情链接、及图片的上传和图像的播放而为上网用户提供按博文主题搜索,查看注册用户的博文及提出相关评论,并为注册用户写留言,游览注册用户的相册、注册等功能的系统。下面就以两种不同的用户来分析博客网站的需求。 4.2.1注册博友 首先必须在博客首页中登录填写用户名和密码,这样才能执行一些相关操作,不然就是普通用户只能查看一些信息,而不能发表博文。可以在管理页面上添加博文的分类,可以上传图片和游览自己的相册,在上传过程中可以将一张图片定义为自己的签名,在个人管理页面中注册用户还可以修改自己的个人信息。博文管理,友情链接管理及博文分类管理,用例图2。 图2 博客管理页面的用例图 在博客主界面中发表博文时可以选择博文类型,这样可以更好管理自己的博文。并查看和删除网友对自己的博文的一些评论、查看和删除网友留下的一些留言,提供与其他网友交流的空间。更好交流,在信息时代人们通过个人空间沟通也用来但不能在自己的博客主页面中提交评论和留言。。这样在本系统中就可以为博友提供更多的个人色彩。在管理页面中添加和删除友情连接。这样博友可以在自己空间中快速定位自己的关心的网站。这样还可以让网友看到自己的一些信息。友情链接及网页访问量统计显示:在博客的个人页面中还提供了推荐给普通网络用户的相关友情链接,此外,对个人页面的访问量也在随时进行统计,并在个人页面中进行直观的显示。 博客主页面的用例图如图3所示: 图3 博客主页面的用例图 4.2.2 普通用户 在博客注册页面注册成为博客的注册用户,才可以拥有自己独立的空间,进入某一博客页面查看相关文章信息时,可以随时对自己所感兴趣的文章发表评论,同时也可以查看到其他人针对该博文的评论。普通用户在阅读博文时可以在最后留下自己的一些评论。他也可以留下更多的信息。这样他可以写留言。这样他留下的信息就更快地被博友所看到。普通用户进入博友的用例图如图4所示: 图4上网博友在博客主页面的用例图 普通网友可以搜索自己关心的一些数据。并且在搜索结果中提供在结果范围内再次搜索。这样就可以将范围缩小。首页向注册用户提供登陆模块,注册用户在博客网站首页跟上网网友一样可以查看博客和博文推荐。上网网友可以注册成为博客用户,拥有自己独立的空间。这一模块的用例图如图5所示: 图5 博客首页的用例图 4.3 功能模块介绍 4.3.1 博客注册登录管理模块 博客注册登录管理模块用于建立博客网站固定的客户群体,通过记录对应的博客档案,实现对博客信息的后台维护及管理,同时也便于通过博客档案库将网站最新动态及相关企业的信息方便地传达给每一位潜在的客户。 该功能模块实现了以下几个子功能。 (1)新博客在线注册。 (2)博客登录管理。 (3)跳转到博客主页。 只有进行登录并通过身份验证的用户,才可以在个人博客页面发表日志,并借助个人设置实现对个人博客相关信息的管理维护。对于没有经过身份验证的网络用户不允许在博客页面中发表日志,更不允许对博客页面信息进行管理维护。 该模块实现新博客的注册及登录验证功能。其中,注册新博客时会对用户输入的注册信息进行有效性验证,包括基本数据格式的有效性以及逻辑有效性,例如,用户名被占用时将及时给出提示。注册成功的博客登录时,会随时根据博客输入的登录信息进行提示,如用户名错误或者密码错误。 4.3.2 博客及文章检索查询模块 博客及文章检索查询模块为网络用户提供便捷的搜索,以及日志阅读浏览等功能,同时对日志的评论信息、博客推荐也能够及时反馈给网络用户。 该功能模块实现了以下几个子功能。 (1)热门博客页面推荐。 (2)最新博客日志推荐。 (3)日志信息关键字搜索。 该模块能够在网页中随时提供在线的最新日志信息。该信息需要定期更新,网络用户可以随时获得最新日志以及最热门的博客推荐。在客户选择了某个博客或者某个感兴趣的日志后,可以方便地跳转到对应博客页面进行日志的阅读,并和博客进行交流互动。 为了使网络用户尽快定位到所需的博客资料及日志信息,本模块提供了搜索功能,用户可以对所关注的日志信息按照标题进行关键字搜索,以避免用户浏览多个页面来寻找所需的日志信息。 4.3.3博客页面显示模块 当网络用户进入某个人博客主页后,在该页面中将提供博客日志列表的显示,同时为了方便用户浏览,在该模块中提供根据分类名进行日志列表的显示,也提供日志评论的浏览,此外还允许用户在博客页面中发表评论及留言。 该功能模块实现了如下几个子功能。 (1)用户可以分页查看对应的日志内容及评论信息。 (2)用户可以针对日志内容发表评论。 (3)用户可以针对博客进行留言。 (4)用户可以分类查看日志内容。 在该模块中还提供了博客页面统计信息,日志、评论及留言信息分页显示等方便用户的显示效果。 4.3.4 博客个人维护管理模块 博客个人维护管理模块用于实现用户对博客个人主页及相关信息的动态管理。 该功能模块实现了如下几个子功能: (1)日志及日志分类管理。 (2)评论及留言管理。 (3)个人基本信息维护管理。 借助该模块,用户可以随时对个人博客主页中的内容进行增加或修改,包括日志分类信息的更新、评论及留言管理等功能,也允许用户对博客的个人信息进行维护及其管理。 4.4 系统分析 本系统采用严格的J2 EE 应用结构,主要有如下几个分层。 1. 表现层:由JSP 页面组成。 2. MVC 层:使用Struts框架。 3. 业务逻辑层:主要由Spring loC 容器管理的业务逻辑组件组成。 4. DAO 层:由7 个DAO 组件组成,实现类必须继承Spring提供的HibernateDaoSupport。 5. Hibernate 持久层:由7 个PO 组成,并在Hibernate Session 管理下,完成数据库访问。 6. 数据库服务层:使用MySQL 数据库存储持久化数据。 系统的具体分层如图5 所示。 图6 系统结构图 在图6 黑色大方框内的MVC 控制层、Service 层及DAO 组件层的组件,都由Spring IOC 容器负责生成,并管理组件的实例(实例必须是单身模式的,本系统中的bean基本上是单身的)。 4.3.1系统架构说明 本系统不仅严格按MVC 模式设计,还按J2 EE 分层设计,将中间层严格分成业务逻辑层、DAO 层及数据持久层等。MVC 层的控制器绝对禁止持久层访问,甚至不参与业务逻辑的实现。表现层采用传统JSP 技术。 本系统采用的是典型的J2EE 三层结构,分为表现层、中间层(业务逻辑层)和数据服务层。三层体系将业务规则、数据访问及合法性校验等工作放在中间层处理。客户端不直接与数据库交互,而是通过组件与中间层建立连接,再由中间层与数据库交互。Jsp广泛的应用和稳定的表现,为其作为表现层技术打下了坚实的基础。中间层采用的是流行的Spring+Hibernate ,为了将控制层与业务逻辑层分离,又细分为以下几种。 Web 层,就是MVC 模式里面的C,负责逻辑层和表现层的交互。调用业务逻辑层,并将业务数据返回给表现层作组织表现,该系统的MVC采用Struts框架。 Service 层(就是业务逻辑层),负责实现业务逻辑。业务逻辑层以DAO 层为基础,通过对DAO 组件的正面模式包装,完成系统所要求的业务逻辑。 DAO 层,负责与持久化对象交互。该层封装了数据的增、删、查、改的操作。 PO ,持久化对象。通过实体关系映射工具将关系型数据库的数据映射成对象,很方便地实现以面向对象方式操作数据库,该系统采用Hibernate 作为ORM 框架。Spring 的作用贯穿了整个中间层,将Web 层、Service 层、DAO 层及PO 无缝整合,其数据服务层用来存放数据。 通过使用Hibernate 持久层,可以避免使用传统的JDBC 操作数据库,对JDBC近一步包装,从而更好地使用面向对象的方式来操作数据库。保证了整个软件开发过程以面向对象的方式进行,即面向对象分析、设计及编程,透过Hibernte 对PO对象持久化操作,不管插入还是查询都是通过PO。 4.3.2实现DAO 层 DAO 还有助于提升系统的可移植性。独立的DAO 层使得系统能在不同的数据库之间轻易切换,底层的数据库实现对于业务逻辑组件是透明的。数据库移植时仅仅影响DAO 层,不同数据库的切换不会影响业务逻辑组件,因此提高了系统的可复用性。 对于不同的持久层技术, Spring 的DAO 提供一个DAO 模板,将通用的操作放在模板里完成,而对于特定的操作,则通过回调接口完成。Spring 为Hibernate 提供的DAO 支持类是: HibernateDaoSupport。 4.3.3 DAO组件的定义 DAO 组件提供了各持久化对象的基本的CRUD 操作。而在DAO 接口里则对DAO组件包含的各种CRUD 方法提供了声明,但有一些IDE 工具也可以生成基本的CRUD方法。使用DAO 接口的原因是:避免业务逻辑组件与特定的DAO组件藕合。由于DAO 组件中的方法不是开始就设计出来的,其中的很多方法可能会随着业务逻辑的需求而增加,但以下几个方法是通用 的。 • get: 根据主键加载持久化实例。 • saveor update: 保存或更新持久化实例。 • remove: 删除持久化实例。 上面涉及了7个PO,这样我们必须设计7个对应的PODao 7个Dao必须继承BaseDao 这个BaseDao 有对接口的一些基本的CURD操作。7个Dao 如下。LinksDao ,BlogDao、FeedBackDao、ArticleDao、MessageDao、SortDao、PictureDao。这7个Dao 分别封装对自己的持久化对象的一些操作。 4.3.4 部署DAO 层 HibernateDaoSupport类只需要一个SessionFactory 属性,即可完成数据库访问。SessionFactroy创建Session,而数据库的CRUD操作都是有Session 完成,并将查询结果保存在一级缓存中,每次用户提交一次会话,可能需要Session完成一些数据库的操作而实际的数据库访问由模板类HibernateTemplate完成,该模板类提供了大量便捷的方法,简化了数据库的访问。 第五章 数据库设计 5.1定义(数据词典) 1、数据库表名的定义使用:重点字_table;重点字可以是多个英文单词的组合,从组合的第二单词起首字母大写,字段命名是能表达字段内容的英文单词的组合,组合方式同表名重点字相同。 2、NN 表示not null 不填表可为空。 3、数据类型不使用固定数据库的类型,标识大众类型,如字符串、字符、数字等。这样在更改数据库时需求的数据是可移植的 4、输写数据表在数据库中的物理名称,可用自己熟悉的语言再定义表名称,方便沟通。 5、长度定义又数据类型决定:如字符串可选8000以下,数字可在50位之间,字符只允许一位。 6、输入方式表示数据的来源:生成表由程序或数据库的索引自动生成,而不需人工录入;输入表示通过文本框等输入的数据;选择表单选框、复选框、下拉列表等通过选择输入的数据;自定获取是为外码的输入设定的,它由程序自动获取。 5.2主要表结构如下 1. 表[blogs]日志数据表 字段名 类型 说明 blogid Int(10) 日志的ID title text 日志标题 pubtime Int(11) 日志发表时间 authorid Int(8) 日志作者的UID replies Int(8) 日志的评论数 tbs Int(8) 日志的trackback数 views Int(8) 日志的查看次数 category Int(3) 日志所属的分类的ID content mediumtext 日志正文内容 property Int(1) 日志的属性 0 公开日志 1 锁定日志 2 隐藏日志 3 一般草稿 2.表[user]博友登陆信息表 字段名 类型 说明 userid Int(10) 博友ID nicheng Text 博友在博客里的称呼 username text 登陆时的用户名 password text 登陆密码 3.表[register]用户数据表 字段名 类型 说明 userid Int(8) 登陆时需要用到的用户名 username text 用户昵称 userpwd Int(8) 登陆密码 regtime Int(11) 注册时间 usergroup Int(2) 所属的用户组ID email text 用户email qq text Qq号 msn text 用户msn gender text 用户性别 第六章 主要功能的实现 6.1 登录控制: if(this.session().getAttribute("rand")!=null){//检验随机数是否为空 if(this.checkCode.equals(this.session().getAttribute("rand").toString())){//比较随机数 uservo=userbo.queryUserByINfo(uservo);//检验用户信息并将信息返回 if(uservo!=null){//看用户是否存在 this.setUserpurview(uservo.getPurviewId());//设置用户权限 this.session().setAttribute("user", uservo);//将用户信息放在session中 return SUCCESS;//返回用户主界面页面 }else{ return ERROR;返回到失败页面} }else{return ERROR; }}else{ return ERROR; } 6.2 Ajax 读取txt格式数据 function ajax_function(var1,var2){ var xmlHttp = null; //创建xmlhttprequest try { // Firefox, Opera 8.0+, Safari 对不同浏览器常见不同对象 xmlHttp=new XMLHttpRequest(); } catch (e) { try { // Internet Explorer xmlHttp=new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { try { xmlHttp=new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) { alert('Your browser does not support AJAX!'); return false; } } } xmlHttp.onreadystatechange=function() { if(xmlHttp.readyState == 4 && xmlHttp.status == 200) { var someDiv = document.getElementById('someDiv'); someDiv.innerHTML=xmlHttp.responseText; } } xmlHttp.open('GET','www/index.jsp?param1='+var1+'¶m2='+var2,true); xmlHttp.send(null); } 6.3 系统架构与数据库的连接 由于我们开发时是利用MVC的设计模式,所以在此我们的数据库连接是封存在M中的,即只要我们和M层连接上,就已经连接上数据库了。 Hibernate使用数据库和配置信息来为应用程序提供持久化服务(以及持久的对象)。我们只需在hibernate.cfg.xml中配置以下语句即可: sa jdbc:microsoft:sqlserver://localhost:1035;DatabaseName=webexamine org.hibernate.dialect.SQLServerDialect sqlserver2000 aaaaaaaa com.microsoft.jdbc.sqlserver.SQLServerDriver org.hibernate.dialect.HSQLDialect true 其实我们在这里配置了一个数据连接,以保证系统的正常运行,我们需要使用proxool连接池,我们经测试,分别用proxool、tomcat JNDI、Hibernate自带的连接池进行配置之后分别执行以上的10000条数据插入和读取,结果显示proxool的性能要优于tomcat JNDI,而Hibernate自带的就更不用提了,差很远。依次为:39265毫秒/10000条、26013毫秒/10000条、50029毫秒/10000条。 6.4数据的备份与还原 我们根据Mysql数据库提供的backup(备份)命令及restore(恢复)命令,进行数据库的备份与还原,在连接数据库后,我们在需要备份和还原的地方加入以下语句: backup database to disk='备份文件名' restore database from disk='备份文件名' 然后发送给数据库的管理对象进行数据还原与备份。 在知道了数据库备份与还原的接口后,用JAVA进行具体的连接,这里采用JAVA直连的方 法,部分代码如下: try{ String user="sa";//连接sqlserver2000数据库登陆名 String password="aaaaaaaa";//连接sqlserver2000数据库密码 String sDBDriver="com.microsoft.jdbc.sqlserver.SQLServerDriver";//连接数据库的驱动字符串 String connStr="jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=webexamine";//注册连接字符串 Connection con=null; Class.forName( sDBDriver);//注册数据库驱动 con = Driver Manager.getConnection(connStr ,user,password);//获取数据库连接 String sql="backup database xncsims to disk='d:\\xncback.dat'";//备份数据库,还原数据库和着类似,只是将这句变为还原的语句 st=con.createStatement();//创建发送对象 rs=st.executeQuery(sql);//这里就是把你的SQL语句发到数据库执行 }catch(SQLException e){ System.out.println(e.toString());} catch(Exception e){ System.out.println(e.toString());} 6.4部分效果预览图 主页 留言界面
第一章 绪论 1.1. 选题背景 这几年,许多新名词涌入我们的视野:博客、圈子、播客、WAP等。这些都预示着我们进入了一个新的互联网阶段web 2.0,它是相对web 1.0的新的一类互联网应用的总称,是一次从核心内容到外部应用的革命[10]。这个阶段发展迅速,互联网应用趋于多样化,其中变化最大的是由web 1.0网站创造内容的时代变为由用户创造内容的web 2.0时代。 在web 2.0应用中,博客(Blog)是web 2.0核心应用中最典型、最流行的代表之一,也是web 2.0技术应用的最直观的表现,是web 2.0精神和理念的具体体现。 1.2. 问题的提出 Blog记载了日常发生的事情和自己的兴趣爱好,把自己的思想和知识和他人分享、交流,同时又通过“六度空间”结识了更多志趣相投的朋友;而越来越多专业知识的 Blog 的出现,让我们看到了 Blog 更多所蕴涵的巨大的信息价值:不同的 Blog 选择不同的内容,收集和整理成为很多人关注的专业 Blog ——目前越来越多的人获取信息的来源是一些固定的 Blog 。随着博客人数的增加, Blog 作为一种新的生活方式、新的工作方式、新的学习方式已经被越来越多的人所接受,并且在改变传统的网络和社会结构:网络信息不再是虚假不可验证的,交流和沟通更有明确的选择和方向性,单一的思想和群体的智慧结合变的更加有效,个人出版变成人人都可以实现的梦想—— Blog 正在影响和改变着我们的生活。 1.3. 系统的开发目标 管理员通过前台页面进入后台管理模块后,可对注册的博客用户进行维护,包括对注册用户的添加、查找、修改和删除。 管理员进入登录后,可对帐户进行管理,包括添加管理员帐户、修改管理员帐户、删除管理员帐户和对帐户进行权限设置。 博客用户通过前台登录后,可对自己的博客空间进行管理,包括发布自己的网络日志、收藏个人图片、和相关人员进行交流和沟通以及删除访问者发表的评论等。 因此,在具体设计实现该博客网站时,主要考虑了主流博客网站的几个主要功能:(1)博客的注册、登录验证功能(2) 网络用户通过关键字搜索博文功能(3) 最热门博客页面推荐浏览(4) 文章详细内容及相关评论显示(5) 博客页面访问量统计(6) 博客个人文章管理维护功能(7) 博客个人文章分类管理维护功能(8) 博客个人友情链接维护功能(9) 博客个人基本信息管理维护功能(10) 博客图片上传及个人相册管理(11) 网络用户写留言,博主查看留言。 第二章 系统设计 2.1. 系统分析 在整个blog进行开发之前,要确定出整个项目的整体架构,包括系统的选型、运行环境的确定及系统结构设计。下面对这进行详细介绍。 在进行软件系统开发的最初环节,一般都需要进行系统的选型,即根据系统功能的实际需求,选择合适的开发工具及软件架构。 blog对系统的可靠性、稳定性有比较高的要求。本系统设计时,比较主流的B/S设计有基于JSP、ASP、PHP、CGI及J2EE等模式。相比较而言PHP的功能相对简单,不适合做大程序;而CGI效率相对较低,所以也不考虑。由于J2EE的开源的框架中提供了MVC模式实现框架Struts、对象关系模型中的Hibernate 的框架及拥有事务管理和依赖注入的Spring。利用现存框架可以更快开发系统。所以选择Java技术作为blog 的开发工具。 为了增加系统的吞吐量,提高并发处理客户请求数量,系统采用了IBM服务器作为主机。在数据库处理方面,不需要在数据层借助存储过程及数据库服务器端函数封装过多的业务逻辑,因此数据库系统采用相对精巧的MySQL[6]。 该在线博客系统服务器端如果需要布置到其他主机上,则该主机必备条件如下: 1. 服务器端操作系统:独立于操作系统的跨平台系统,客户端MicroSoft Windows 2000及以上; 2. 数据库:MySQL 5.0.27版本; 3. Web服务器:Tomcat 5.5及以上版本,配合MVC设计模式及 Hibernate开发架构; 4. 客户端运行环境:能运行IE 5以上或Netscape 5以上浏览器的操作系统,配合使用Ajax技术; 5. 客户端运行工具:目前的系统采用浏览器作为客户端,为了支持Ajax开发框架,应该选择使用IE 5以上版本浏览器。 本网站以xp为Web平台,JSP+Ajax+Servlet+JavaBean+Hibernate为网站实现技术,建立基于MySQL数据库系统的核心动态网页,实现博客网站前台及博客个人维护管理等功能模块。 1、 系统处理的准确性和及时性:系统处理的准确性和及时性是系统的必要性能。在系统设计和开发过程中,要充分考虑系统当前和将来可能承受的工作量,使系统的处理能力和响应时间能够满足信息处理的需求。 2、 系统的开放性和系统的可扩充性:系统在开发过程中,应该充分考虑以后的可扩充性。例如数据表中用户选择字段方式的改变,用户查询的需求也会不断的更新和完善。所有这些,都要求系统提供足够的手段进行功能的调整和扩充。而要实现这一点,应通过系统的开放性来完成,既系统应是一个开放系统,只要符合一定的规范,可以简单的加入和减少系统的模块,配置系统的硬件。通过软件的修补、替换完成系统的升级和更新换代。 3、 系统的易用性和易维护性:要实现这一点,就要求系统应该尽量使用用户熟悉的术语和中文信息的界面;针对用户可能出现的使用问题,要提供足够的在线帮助,缩短用户对系统熟悉的过程。 4、 系统的数据要求:1、数据录入和处理的准确性和实时性。2、数据的一致性与完整性。3、数据的共享与独立性。 2.2. 系统的可行性分析 2.2.1. 技术可行性 技术上的可行性分析要考虑将来要采用的硬件和软件技术能否满足用户(这里是服务器,网速)提出的要求(如计算机的容量、速度等)。此外,还要考虑开发人员的水平,学习了两年的jsp开发,对于这个系统的编写,我想完整的之需要两个月就可以写出程序,再花上几天的调试,计划两个月左右就可以完成投入使用了。 我们掌握了数据库及其应用技术、数据库原理、计算机网络技术等课程,对数据库的设计、应用、维护及局域网的组成有了深刻的认识与一定的动手实践能力,考取了信息处理、程序设计、数据库技术等国家IT认证。从一定程度上具备了开发一个小型系统的能力。再有就! 2.2.2. 经济可行性 主要从对项目的经济上进行分析评价,一方面是支出的费用,包括设备购置费、管理和维护费用、人员工资和培训费等,另一个是取得的收益。这是个小型的系统,从投入的人力,财力与物力来讲是非常小的,发布出去之需要注册域名就可以了,从节省人力方面,可以让管理人员从繁与复杂的工作中解脱出来,做更多的工作。 2.2.3. 管理可行性 有IP地址、用户名与密码等,可以下载一个FTP上传工具(cure)上传更新后的内容.可以在http://www.skycn.com/soft/683.html下载到. 下载以后,通过"站点管理"-->"新建站点".输入IP,用户名,密码.然后连接即可. 当然也可用FTP工具.直接在IE浏览器地址栏输入ftp://www.域名/然后输入用户名和密码.同样可以上传.随时更新文件! 第三章 开发环境的说明与安装 3.1. 开发语言的选择 3.1.1. JAVA简介 Java是Sun公司推出的新的一代面向对象程序设计语言,特别适合于Internet应用程序开发。Java的产生与流行是当今Internet发展的客观要求,Java是一门各方面性能都很好的编程语言,它的基本特点是简单、面向对象、分布式、解释的、健壮的、安全的、结构中立的、可移植的、性能很优异的、多线程的、动态的,特别适合在Internet环境上开发的应用系统。 3.1.2. Web应用程序开发环境—JSP技术 JSP的全称是Java Server Pages,它是SUN推出的一种动态网页技术标准。它在传统的静态页面文件(*.html,*.htm)中加入JAVA程序片段和JSP标记,就构成了JSP页面。JSP具有以下的优点: 1、将业务层与表示层分离:使用JSP技术,网络开发人员可充分使用HTML来设计页面显示部分(如字体颜色等),并使用JSP指令或者JAVA程序片段来生成网页上的动态内容; 2、能够跨平台:JSP支持绝大部分平台,包括现在非常流行的LINUX系统,应用非常广泛的Apache服务器也提供了支持JSP的服务; 3、组件的开发和使用很方便:如ASP的组件是由C++,VB等语言开发的,并需要注册才能使用;而JSP的组件是用Java开发的,可以直接使用; 4、一次编写,处处运行:作为JAVA开发平台的一部分,JSP具有JAVA的所有优点,包括Write once , Run everywhere. 3.2. 数据库的选择 3.2.1. Web应用程序开发环境—SQLserver数据库 SQL Server是由Microsoft开发和推广的关系数据库管理系统(DBMS),它最初是由Microsoft、Sybase和Ashton-Tate三家公司共同开发的。SQL Server 2000是Microsoft公司于2000年推出的最新版本。 SQL Server 特点:   1.真正的客户机/服务器体系结构。   2.图形化用户界面,使系统管理和数据库管理更加直观、简单。   3.丰富的编程接口工具,为用户进行程序设计提供了更大的选择余地。   4.SQL Server与Windows NT完全集成,利用了NT的许多功能,如发送和接受消息,管理登录安全性等。SQL Server也可以很好地与Microsoft BackOffice产品集成。   5.具有很好的伸缩性,可跨越从运行Windows 95/98的膝上型电脑到运行Windows 2000的大型多处理器等多种平台使用。   6.对Web技术的支持,使用户能够很容易地将数据库中的数据发布到Web页面上。   7.SQL Server提供数据仓库功能,这个功能只在Oracle和其他更昂贵的DBMS中才有。   SQL Server 2000与以前版本相比较,又具有以下新特性 :   1.支持XML(Extensive Markup Language,扩展标记语言)   2.强大的基于Web的分析   3.支持OLE DB和多种查询   4.支持分布式的分区视图   安装、运行SQL Server 2000的硬件需求   (1)计算机   Inter及其兼容计算机,Pentium 166Mz或者更高处理器或DEC Alpha和其兼容系统。   (2)内存(RAM)   企业版最少64MB内存,其他版本最少需要32MB内存,建议使用更多的内存。   (3)硬盘空间   完全安装(Full)需要180MB的空间,典型安装(Typical)需要170MB的空间,最小安装(Minimum)需要65MB的空间。 3.3. 开发工具的选择 MyEclipse,Deamweare,选择SQLserver作为后台的数据库,选择JAVA、JSP、JavaScript、Html作为应用程序开发工具,运用Tomcat服务器技术,整个系统完全基于B/S (Browser/Server)模式进行设计。 1、Tomcat应用服务器 目前支持JSP的应用服务器是较多的,Tomcat是其中较为流行的一个Web服务器,被JavaWorld杂志的编辑选为2001年度最具创新的Java产品,可见其在业界的地位。 Tomcat是一个免费的开源的Serlvet容器,在Tomcat中,应用程序的部署很简单,你只需将你的WAR放到Tomcat的webapp目录下,Tomcat会自动检测到这个文件,并将其解压。你在浏览器中访问这个应用的Jsp时,通常第一次会很慢,因为Tomcat要将Jsp转化为Servlet文件,然后编译。编译以后,访问将会很快。 Tomcat也具有传统的Web服务器的功能:处理Html页面。但是与Apache相比,它的处理静态Html的能力就不如Apache。我们可以将Tomcat和Apache集成到一块,让Apache处理静态Html,而Tomcat处理Jsp和Servlet。Tomcat是一个很好的工具,不仅仅因为其免费,功能强大,更因为其开放性,越来越受到人们的重视。 2、 B/S 开发模式 伴随着Internet的迅速发展,计算机技术正在由基于C/S(client/ Server)模式的应用系统转变为基于B/S模式的应用系统。 过去,网络软件的开发都采用C/S(client)模式,在这种模式下,主要的业务逻辑都集中于客户端程序,因此,必然导致以下问题: 系统安装、调试、维护和升级困难。由于客户端的硬件配置可能存在差异,软件环能各不相同,因此,在安装时,必须对每一个客户端分别进行配置,同样,在软件升级时也要对客户端分别处理。 B/S模式带来了巨大的好处: 开发成本及维护成本降低。由于B/S架构管理软件只安装在服务器端(Server)上,即应用程序在部署、升级、维护时,只需要在服务器端进行配置就可以了,网络管理人员只需要管理服务器就行了,用户界面主要事务逻辑在服务器(Server)端完全通过WWW浏览器实现,极少部分事务逻辑在前端(Browser)实现,所有的客户端只有浏览器,网络管理人员只需要做硬件维护。 良好的安全性能,防火墙技术可以保证后台数据库的安全性。所有的配置工作都集中在服务器端且所有客户端请求都是通过DBMS来访问数据库,从而大大减少了数据直接暴露的风险。 第四章 系统设计 4.1开发框架技术介绍 对于框架技术,我们采用Struts+ajax的整合! Struts是应用比较广泛的一种表现层框架 1、 Struts是一个为开发基于模型(Model)-视图(View)-控制器(Controller)(MVC)模式的应用架构的开源框架,是利用Servlet,JSP和custom tag library构建Web应用的一项非常有用的技术。由于Struts能充分满足应用开发的需求,简单易用,敏捷迅速。它使用服务层框架可以将JavaBeans从Jsp/Servlet中分离出来,而使用表现层框架则可以将Jsp中剩余的JavaBeans完全分离,这部分JavaBeans主要负责显示相关信息,一般是通过标签库(Taglib)实现,不同框架有不同自己的标签库,Struts是应用比较广泛的一种表现层框架。 2、 Ajax我们主要应用就是xmlhttprequest,回调函数实现局部刷新达道数据更新! 4.2需求分析 Blog网站主要是实现注册用户登录、管理相关信息、博文及相关评论、查看留言、友情链接、及图片的上传和图像的播放而为上网用户提供按博文主题搜索,查看注册用户的博文及提出相关评论,并为注册用户写留言,游览注册用户的相册、注册等功能的系统。下面就以两种不同的用户来分析博客网站的需求。 4.2.1注册博友 首先必须在博客首页中登录填写用户名和密码,这样才能执行一些相关操作,不然就是普通用户只能查看一些信息,而不能发表博文。可以在管理页面上添加博文的分类,可以上传图片和游览自己的相册,在上传过程中可以将一张图片定义为自己的签名,在个人管理页面中注册用户还可以修改自己的个人信息。博文管理,友情链接管理及博文分类管理,用例图2。 图2 博客管理页面的用例图 在博客主界面中发表博文时可以选择博文类型,这样可以更好管理自己的博文。并查看和删除网友对自己的博文的一些评论、查看和删除网友留下的一些留言,提供与其他网友交流的空间。更好交流,在信息时代人们通过个人空间沟通也用来但不能在自己的博客主页面中提交评论和留言。。这样在本系统中就可以为博友提供更多的个人色彩。在管理页面中添加和删除友情连接。这样博友可以在自己空间中快速定位自己的关心的网站。这样还可以让网友看到自己的一些信息。友情链接及网页访问量统计显示:在博客的个人页面中还提供了推荐给普通网络用户的相关友情链接,此外,对个人页面的访问量也在随时进行统计,并在个人页面中进行直观的显示。 博客主页面的用例图如图3所示: 图3 博客主页面的用例图 4.2.2 普通用户 在博客注册页面注册成为博客的注册用户,才可以拥有自己独立的空间,进入某一博客页面查看相关文章信息时,可以随时对自己所感兴趣的文章发表评论,同时也可以查看到其他人针对该博文的评论。普通用户在阅读博文时可以在最后留下自己的一些评论。他也可以留下更多的信息。这样他可以写留言。这样他留下的信息就更快地被博友所看到。普通用户进入博友的用例图如图4所示: 图4上网博友在博客主页面的用例图 普通网友可以搜索自己关心的一些数据。并且在搜索结果中提供在结果范围内再次搜索。这样就可以将范围缩小。首页向注册用户提供登陆模块,注册用户在博客网站首页跟上网网友一样可以查看博客和博文推荐。上网网友可以注册成为博客用户,拥有自己独立的空间。这一模块的用例图如图5所示: 图5 博客首页的用例图 4.3 功能模块介绍 4.3.1 博客注册登录管理模块 博客注册登录管理模块用于建立博客网站固定的客户群体,通过记录对应的博客档案,实现对博客信息的后台维护及管理,同时也便于通过博客档案库将网站最新动态及相关企业的信息方便地传达给每一位潜在的客户。 该功能模块实现了以下几个子功能。 (1)新博客在线注册。 (2)博客登录管理。 (3)跳转到博客主页。 只有进行登录并通过身份验证的用户,才可以在个人博客页面发表日志,并借助个人设置实现对个人博客相关信息的管理维护。对于没有经过身份验证的网络用户不允许在博客页面中发表日志,更不允许对博客页面信息进行管理维护。 该模块实现新博客的注册及登录验证功能。其中,注册新博客时会对用户输入的注册信息进行有效性验证,包括基本数据格式的有效性以及逻辑有效性,例如,用户名被占用时将及时给出提示。注册成功的博客登录时,会随时根据博客输入的登录信息进行提示,如用户名错误或者密码错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值