C 语言编写聊天室

  • 一、需求
  • 二、知识点
    • 1、TCP/IP协议
      • (1)C/S模型
      • (2)常规步骤
      • (3)阻塞与非阻塞 socket
      • (4)epoll
    • 2、文件操作
    • 3、数据库
  • 三、实现
    • 1、思路
    • 2、代码
      • (1)utility.h
      • (2)server.c
      • (3)client.c
    • 3、运行结果
  • 参考

一、需求

  • 基于TCP编写,一个聊天室C/S。

  • 基本要求

    • 支持多个用户接入,实现聊天室的基本功能
    • 使用epoll机制实现并发、增加效率
    • 使用fork创建两个进程
    • 将聊天信息写到管道(pipe),并发送给父进程
    • 使用epoll机制接受服务端发来的信息,并显示给用户,使用户看到其他用户的聊天信息

  • 暂且不含线程池、多线程编程、超时重传、确认收包等

  • 后续待加功能

    • 实现简单的文件传输。
    • 用户注册和登陆

二、知识点

1、TCP/IP协议

(1)C/S模型

(2)常规步骤

  • TCP服务端

    • 1、使用 socket()创建 TCP 套接字(socket)
    • 2、将创建的套接字绑定到一个本地地址和端口上(Bind)
    • 3、将套接字设为监听模式,准备接收客户端请求(listen)
    • 4、等待客户请求到来: 当请求到来后,接受连接请求,返回一个对应于此次连接的新的套接字(accept)
    • 5、用 accept 返回的套接字和客户端进行通信(使用write()/send()或send()/recv() )
    • 6、返回,等待另一个客户请求
    • 7、关闭套接字
//server.cpp代码(通信模块)://服务端地址 ip地址 + 端口号struct sockaddr_in serverAddr;serverAddr.sin_family = PF_INET;serverAddr.sin_port = htons(SERVER_PORT);serverAddr.sin_addr.s_addr = inet_addr(SERVER_HOST);//服务端创建监听socketint listener = socket(PF_INET, SOCK_STREAM, 0);if(listener < 0) { perror("listener"); exit(-1);}printf("listen socket created \n");//将服务端地址与监听socket绑定if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {perror("bind error");exit(-1);}//开始监听int ret = listen(listener, 5);if(ret < 0) { perror("listen error"); exit(-1);}printf("Start to listen: %s\n", SERVER_HOST);
  • TCP客户端

    • 1、创建套接字(socket)
    • 2、使用 connect() 建立到达服务器的连接(connect)
    • 3、客户端进行通信(使用 write()/send() 或 send()/recv() )
    • 4、使用 close() 关闭客户连接
//client.cpp代码(通信模块)://客户要连接的服务端地址( ip地址 + 端口号)struct sockaddr_in serverAddr;serverAddr.sin_family = PF_INET;serverAddr.sin_port = htons(SERVER_PORT);serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);// 创建套接字(socket)int sock = socket(PF_INET, SOCK_STREAM, 0);if(sock < 0) { perror("sock error"); exit(-1); }//向服务器发出连接请求(connect)if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {perror("connect error");exit(-1);}

(3)阻塞与非阻塞 socket

分类 阻塞 非阻塞
概念 对一个文件描述符指定的文件或设备, 一般有两种工作方式: 阻塞与非阻塞方式
当试图对该文件描述符进行读写时,如果当时没有数据可读,或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止 如果没有数据可读,或者不可写,读写函数马上返回,而不会等待
举例 比如说小明去找一个女神聊天,女神却不在
如果小明舍不得走,只能在女神大门口死等着,当然小明可以休息。当女神来了,她会把你唤醒(囧,因为挡着她门了),这就是阻塞方式 如果小明舍不得走,只能在女神大门口死等着,当然小明可以休息。当女神来了,她会把你唤醒(囧,因为挡着她门了),这就是阻塞方式
区别 等待 立即返回
  • 本项目采用更高效的做法,所以应该将socket设置为非阻塞方式。这样能充分利用服务器资源,效率得到了很大提高本项目采用更高效的做法,所以应该将socket设置为非阻塞方式。这样能充分利用服务器资源,效率得到了很大提高
//utility.h代码(设置非阻塞函数模块):
//将文件描述符设置为非阻塞方式(利用fcntl函数)
int setnonblocking(int sockfd)
{fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);return 0;
}

(4)epoll

  • 当服务端的在线人数越来越多,会导致系统资源吃紧,I/O 效率越来越慢,这时候就应该考虑 epoll 了。epoll 是 Linux 内核为处理大批句柄而作改进的 poll ,是 Linux 特有的 I/O 函数。
  • 其特点如下:
    • epoll 是 Linux 下多路复用 IO 接口 select/poll 的增强版本。其实现和使用方式与 select/poll 有很多不同,epoll 通过一组函数来完成有关任务,而不是一个函数。

    • epoll 之所以高效,是因为 epoll 将用户关心的文件描述符放到内核里的一个事件表中,而不是像 select/poll 每次调用都需要重复传入文件描述符集或事件集。比如当一个事件发生(比如说读事件),epoll 无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入就绪队列的描述符集合就行了。

    • 与select相比,epoll分清了频繁调用和不频繁调用的操作

    • epoll 有两种工作方式,LT(level triggered):水平触发和 ET(edge-triggered):边沿触发。LT 是 select/poll 使用的触发方式,比较低效;而 ET 是 epoll 的高速工作方式(本项目使用 epoll 的 ET 方式)。

    • 通俗理解就是,比如说有一堆女孩,有的很漂亮,有的很凤姐。现在你想找漂亮的女孩聊天,LT 就是你需要把这一堆女孩全都看一遍,才可以找到其中的漂亮的(就绪事件);而ET是你的小弟(内核)将N个漂亮的女孩编号告诉你,你直接去看就好,所以 epoll 很高效。另外,还记得小明找女神聊天的例子吗?采用非阻塞方式,小明还需要每隔十分钟回来看一下(select);如果小明有小弟(内核)帮他守在大门口,女神回来了,小弟会主动打电话,告诉小明女神回来了,快来处理吧!这就是 epoll。

    • epoll 共3个函数, 如下:

    • int epoll_create(int size)
      //创建一个epoll句柄,参数size用来告诉内核监听的数目,size为epoll所支持的最大句柄数

    • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
      //函数功能: epoll事件注册函数
      //参数epfd为epoll的句柄,即epoll_create返回值
      //参数op表示动作,用3个宏来表示:
      EPOLL_CTL_ADD//(注册新的fd到epfd),
      EPOLL_CTL_MOD//(修改已经注册的fd的监听事件),
      EPOLL_CTL_DEL//(从epfd删除一个fd);
      //其中参数fd为需要监听的标示符;
      //参数event告诉内核需要监听的事件,event的结构如下:
      struct epoll_event {
      __uint32_t events; //Epoll events
      epoll_data_t data; //User data variable
      };
      //其中介绍events是宏的集合,本项目主要使用EPOLLIN(表示对应的文件描述符可以读,即读事件发生),其他宏类型,可以google之!

    • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
      等待事件的产生,函数返回需要处理的事件数目(该数目是就绪事件的数目,就是前面所说漂亮女孩的个数N)

    • 服务端使用 epoll 的时候,步骤如下:
      1、 调用 epoll_create 函数在 Linux 内核中创建一个事件表;
      2、然后将文件描述符(监听套接字 listener )添加到所创建的事件表中;
      3、在主循环中,调用 epoll_wait 等待返回就绪的文件描述符集合;
      4、分别处理就绪的事件集合,本项目中一共有两类事件:新用户连接事件和用户发来消息事件( epoll 还有很多其他事件,本项目为简洁明了,不介绍)。

    • 举例:下面介绍下如何将一个 socket 添加到内核事件表中

//utility.h(添加 socket 模块):
//将文件描述符 fd 添加到 epollfd 标示的内核事件表中, 并注册 EPOLLIN 和 EPOOLET 事件,
//EPOLLIN 是数据可读事件;EPOOLET 表明是 ET 工作方式。最后将文件描述符设置非阻塞方式
/*** @param epollfd: epoll句柄* @param fd: 文件描述符* @param enable_et : enable_et = true, 采用epoll的ET工 作方式;否则采用LT工作方式
**/
void addfd( int epollfd, int fd, bool enable_et )
{struct epoll_event ev;ev.data.fd = fd;ev.events = EPOLLIN;if( enable_et )ev.events = EPOLLIN | EPOLLET;epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);setnonblocking(fd);printf("fd added to epoll!\n\n");
}

2、文件操作

3、数据库

三、实现

1、思路

2、代码

(1)utility.h

#ifndef UTILITY_H_INCLUDED
#define UTILITY_H_INCLUDED#include <iostream>
#include <list>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>using namespace std;// clients_list save all the clients's socket
list<int> clients_list;/**********************   macro defintion **************************/
// server ip
#define SERVER_IP "127.0.0.1"// server port
#define SERVER_PORT 8888//epoll size
#define EPOLL_SIZE 5000//message buffer size
#define BUF_SIZE 0xFFFF#define SERVER_WELCOME "Welcome you join  to the chat room! Your chat ID is: Client #%d"#define SERVER_MESSAGE "ClientID %d say >> %s"// exit
#define EXIT "EXIT"#define CAUTION "There is only one int the char room!"/**********************   some function **************************/
/*** @param sockfd: socket descriptor* @return 0
**/
int setnonblocking(int sockfd)
{fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);return 0;
}/*** @param epollfd: epoll handle* @param fd: socket descriptor* @param enable_et : enable_et = true, epoll use ET; otherwise LT
**/
void addfd( int epollfd, int fd, bool enable_et )
{struct epoll_event ev;ev.data.fd = fd;ev.events = EPOLLIN;if( enable_et )ev.events = EPOLLIN | EPOLLET;epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);setnonblocking(fd);printf("fd added to epoll!\n\n");
}/*** @param clientfd: socket descriptor* @return : len
**/
int sendBroadcastmessage(int clientfd)
{// buf[BUF_SIZE] receive new chat message// message[BUF_SIZE] save format messagechar buf[BUF_SIZE], message[BUF_SIZE];bzero(buf, BUF_SIZE);bzero(message, BUF_SIZE);// receive messageprintf("read from client(clientID = %d)\n", clientfd);int len = recv(clientfd, buf, BUF_SIZE, 0);if(len == 0)  // len = 0 means the client closed connection{close(clientfd);clients_list.remove(clientfd); //server remove the clientprintf("ClientID = %d closed.\n now there are %d client in the char room\n", clientfd, (int)clients_list.size());}else  //broadcast message {if(clients_list.size() == 1) { // this means There is only one int the char roomsend(clientfd, CAUTION, strlen(CAUTION), 0);return len;}// format message to broadcastsprintf(message, SERVER_MESSAGE, clientfd, buf);list<int>::iterator it;for(it = clients_list.begin(); it != clients_list.end(); ++it) {if(*it != clientfd){if( send(*it, message, BUF_SIZE, 0) < 0 ) { perror("error"); exit(-1);}}}}return len;
}
#endif // UTILITY_H_INCLUDED
  • 介绍
/* 限于篇幅,这里先介绍下utility.h的主要构成 */
//服务端存储所有在线用户socket, 便于广播信息
list<int> clients_list;
// 服务器ip地址,为测试使用本地机地址,可以更改为其他服务端地址
#define SERVER_IP "127.0.0.1"
// 服务器端口号
#define SERVER_PORT 8888
//int epoll_create(int size)中的size,为epoll支持的最大句柄数
#define EPOLL_SIZE 5000// 缓冲区大小65535
#define BUF_SIZE 0xFFFF
//一些宏
#define SERVER_WELCOME "Welcome you join to the chat room! Your chat ID is: Client #%d"
#define SERVER_MESSAGE "ClientID %d say >> %s"
#define EXIT "EXIT"
#define CAUTION "There is only one int the char room!"
/* 一些函数 */
//设置非阻塞
int setnonblocking(int sockfd);
//将文件描述符fd添加到epollfd标示的内核事件表
void addfd( int epollfd, int fd, bool enable_et );
//服务端发送广播信息,使所有用户都能收到消息
int sendBroadcastmessage(int clientfd);

(2)server.c

#include "utility.h"int main(int argc, char *argv[])
{//服务器IP + portstruct sockaddr_in serverAddr;serverAddr.sin_family = PF_INET;serverAddr.sin_port = htons(SERVER_PORT);serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);//创建监听socketint listener = socket(PF_INET, SOCK_STREAM, 0);if(listener < 0) { perror("listener"); exit(-1);}printf("listen socket created \n");//绑定地址if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {perror("bind error");exit(-1);}//监听int ret = listen(listener, 5);if(ret < 0) { perror("listen error"); exit(-1);}printf("Start to listen: %s\n", SERVER_IP);//在内核中创建事件表int epfd = epoll_create(EPOLL_SIZE);if(epfd < 0) { perror("epfd error"); exit(-1);}printf("epoll created, epollfd = %d\n", epfd);static struct epoll_event events[EPOLL_SIZE];//往内核事件表里添加事件addfd(epfd, listener, true);//主循环while(1){//epoll_events_count表示就绪事件的数目int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);if(epoll_events_count < 0) {perror("epoll failure");break;}printf("epoll_events_count = %d\n", epoll_events_count);//处理这epoll_events_count个就绪事件for(int i = 0; i < epoll_events_count; ++i){int sockfd = events[i].data.fd;//新用户连接if(sockfd == listener){struct sockaddr_in client_address;socklen_t client_addrLength = sizeof(struct sockaddr_in);int clientfd = accept( listener, ( struct sockaddr* )&client_address, &client_addrLength );printf("client connection from: %s : % d(IP : port), clientfd = %d \n",inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port),clientfd);addfd(epfd, clientfd, true);// 服务端用list保存用户连接clients_list.push_back(clientfd);printf("Add new clientfd = %d to epoll\n", clientfd);printf("Now there are %d clients int the chat room\n", (int)clients_list.size());// 服务端发送欢迎信息  printf("welcome message\n");                char message[BUF_SIZE];bzero(message, BUF_SIZE);sprintf(message, SERVER_WELCOME, clientfd);int ret = send(clientfd, message, BUF_SIZE, 0);if(ret < 0) { perror("send error"); exit(-1); }}//处理用户发来的消息,并广播,使其他用户收到信息else {   int ret = sendBroadcastmessage(sockfd);if(ret < 0) { perror("error");exit(-1); }}}}close(listener); //关闭socketclose(epfd);    //关闭内核return 0;
}

(3)client.c

  • 子进程与父进程之间的通信

  • 通过调用 int pipe(int fd[2]) 函数创建管道, 其中 fd[0] 用于父进程读, fd[1] 用于子进程写。

//client.cpp代码(管道模块)// 创建管道.int pipe_fd[2];if(pipe(pipe_fd) < 0) { perror("pipe error"); exit(-1); }
  • 通过 int pid = fork() 函数,创建子进程,当 pid < 0 错误;当 pid = 0, 说明是子进程;当 pid > 0 说明是父进程。根据 pid 的值,我们可以父子进程,从而实现对应的功能!
#include "utility.h"int main(int argc, char *argv[])
{//用户连接的服务器 IP + portstruct sockaddr_in serverAddr;serverAddr.sin_family = PF_INET;serverAddr.sin_port = htons(SERVER_PORT);serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);// 创建socketint sock = socket(PF_INET, SOCK_STREAM, 0);if(sock < 0) { perror("sock error"); exit(-1); }// 连接服务端if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {perror("connect error");exit(-1);}// 创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写int pipe_fd[2];if(pipe(pipe_fd) < 0) { perror("pipe error"); exit(-1); }// 创建epollint epfd = epoll_create(EPOLL_SIZE);if(epfd < 0) { perror("epfd error"); exit(-1); }static struct epoll_event events[2]; //将sock和管道读端描述符都添加到内核事件表中addfd(epfd, sock, true);addfd(epfd, pipe_fd[0], true);// 表示客户端是否正常工作bool isClientwork = true;// 聊天信息缓冲区char message[BUF_SIZE];// Forkint pid = fork();if(pid < 0) { perror("fork error"); exit(-1); }else if(pid == 0)      // 子进程{//子进程负责写入管道,因此先关闭读端close(pipe_fd[0]); printf("Please input 'exit' to exit the chat room\n");while(isClientwork){bzero(&message, BUF_SIZE);fgets(message, BUF_SIZE, stdin);// 客户输出exit,退出if(strncasecmp(message, EXIT, strlen(EXIT)) == 0){isClientwork = 0;}// 子进程将信息写入管道else {if( write(pipe_fd[1], message, strlen(message) - 1 ) < 0 ){ perror("fork error"); exit(-1); }}}}else  //pid > 0 父进程{//父进程负责读管道数据,因此先关闭写端close(pipe_fd[1]); // 主循环(epoll_wait)while(isClientwork) {int epoll_events_count = epoll_wait( epfd, events, 2, -1 );//处理就绪事件for(int i = 0; i < epoll_events_count ; ++i){bzero(&message, BUF_SIZE);//服务端发来消息if(events[i].data.fd == sock){//接受服务端消息int ret = recv(sock, message, BUF_SIZE, 0);// ret= 0 服务端关闭if(ret == 0) {printf("Server closed connection: %d\n", sock);close(sock);isClientwork = 0;}else printf("%s\n", message);}//子进程写入事件发生,父进程处理并发送服务端else { //父进程从管道中读取数据int ret = read(events[i].data.fd, message, BUF_SIZE);// ret = 0if(ret == 0) isClientwork = 0;else{   // 将信息发送给服务端send(sock, message, BUF_SIZE, 0);}}}//for}//while}if(pid){//关闭父进程和sockclose(pipe_fd[0]);close(sock);}else{//关闭子进程close(pipe_fd[1]);}return 0;
}

3、运行结果

参考

1、Linux下基于socket和多线程的聊天室小程序
2、Linux C实现简单的网络聊天室
3、C 基于UDP实现一个简易的聊天室
4、C语言利用epoll实现高并发聊天室–实验楼
5、3、UNIX网络编程学习笔记–TCP套接字编程
6、Linux下的I/O复用与epoll详解

C语言练手项目--C 语言编写聊天室相关推荐

  1. C语言练手项目--C 语言制作简单计算器

    C 语言制作简单计算器 一.需求 二.实现 1.思路 2.代码 3.运行结果 三.参考链接 一.需求 使用 C 语言做一个简单的计算器,能执行加.减.乘.除操作 二.实现 1.思路 (1)标准输入到s ...

  2. 【GO语言】实现同步传输系统:局域网内手机和电脑互传文件互发消息。go语言练手项目

    GO语言实现同步传输系统:局域网内手机和电脑互传文件互发消息 项目总览: 一.项目功能展示 1.用手机传输文件到电脑 2.用手机传输图片到电脑 3.用电脑传输文字到手机 4.服务器显示情况,端口信息: ...

  3. c++ 小游戏_C/C++编程笔记:C语言写推箱子小游戏,大一学习C语言练手项目

    C语言,作为大多数人的第一门编程语言,重要性不言而喻,很多编程习惯,逻辑方式在此时就已经形成了.这个是我在大一学习 C语言 后写的推箱子小游戏,自己的逻辑能力得到了提升,在这里同大家分享这个推箱子小游 ...

  4. c语言判断一个点在长方体内部_21个入门练手项目,让你轻松玩转C语言

    C 语言作为大学理工科专业的必修,是很多同学走进编程世界的第一课.那么怎样才能更好的入门 C 语言呢? 下面整理了 21 个 C 语言练手项目,从基础语法开始,逐步深入,通过一个个练手项目,让你轻松驰 ...

  5. c语言21个入门练手项目,21个入门练手项目让你轻松玩转C语言

    整理了 21 个 C 语言练手项目,从基础语法开始,逐步深入,通过一个个练手项目,让你轻松驰骋在 C 语言的快车道.不走弯路就是捷径! 1.<C语言入门教程>:引入大量的 C 语言程序案例 ...

  6. 【C语言】游戏开发:天天酷跑丨完美练手项目 [附源码]

    目录 一.项目说明: 二.项目作用 三.项目技术要求 四.库.宏.主函数说明 五.项目实现 5.1游戏背景的实现 5.2实现Hero奔跑 5.3 实现Hero跳跃 5.4 优化帧等待 5.6使用结构体 ...

  7. 70个Python练手项目列表 预祝大家 快乐

    小孩眺望远方,成人怀念故乡. 为此给大家分享一下珍藏的Python实战项目,祝大家节日快乐哦!!! Python 前言:不管学习哪门语言都希望能做出实际的东西来,这个实际的东西当然就是项目啦,不用多说 ...

  8. 一个适合于Python 初学者的入门练手项目

    随着人工智能的兴起,国内掀起了一股Python学习热潮,入门级编程语言,大多选择Python,有经验的程序员,也开始学习Python,正所谓是人生苦短,我用Python 有个Python入门练手项目, ...

  9. python新手项目-推荐:一个适合于Python新手的入门练手项目

    原标题:推荐:一个适合于Python新手的入门练手项目 随着人工智能的兴起,国内掀起了一股Python学习热潮,入门级编程语言,大多选择Python,有经验的程序员,也开始学习Python,正所谓是人 ...

  10. python新手小项目-推荐:一个适合于Python新手的入门练手项目

    随着人工智能的兴起,国内掀起了一股Python学习热潮,入门级编程语言,大多选择Python,有经验的程序员,也开始学习Python,正所谓是人生苦短,我用Python 有个Python入门练手项目, ...

最新文章

  1. Mac环境下Docker及Splash的安装运行教程
  2. windows下安装Python virtualenvwrapper-win
  3. 如何打开手机端口_微信接收图纸dwg怎么打开?如何手机查看CAD图纸,三步免费教你...
  4. 《机器学习实战》笔记(02):k-近邻算法
  5. Beta版本测试报告
  6. 5.3矩阵的压缩存储(稀疏矩阵转置和快速转置)
  7. 设计模式六大原则(转)
  8. java 容易犯错_Java中容易犯错的题
  9. linux系统下如何创建二级域名和删除这个目录文件
  10. oracle当前用户创建的表不可见?
  11. 设计模式(一)面向对象设计原则
  12. STL之仿函数实现详解
  13. C4—Qt实现记事本(一)2021-11-16
  14. Symbian中的iScanCode和iCode(转)
  15. Android网络验证逆向,【Android逆向】去除无聊的网络验证弹窗与360加固
  16. python3.8零基础入门教程_正版 Python 3.8编程快速入门 针对wan全零基础入门的读者 采用*小化安装+极简代码的教学...
  17. 寒门再难出贵子?学会这个方法,跨越阶层不是梦
  18. 用 TensorFlow 做个聊天机器人
  19. 商业银行业务培训总结
  20. 怎么抓雷电模拟器的包_fiddler+雷电模拟器进行APP抓包(可抓HTTPS)

热门文章

  1. 网站竞价推广抗恶意点击及屏蔽特定地区IP实例
  2. mysqldump: Got error: 1044
  3. android随机摇号代码,抽奖摇号系统随机性算法介绍
  4. Java学习的准备工作
  5. python 字体颜色_Python字体颜色设置
  6. 企业发文的红头文件_【红头文件写作格式】 公司红头文件格式范本
  7. InnoDB存储引擎学习笔记(更新ing)
  8. linux syn发包工具,发包工具 TRex stateless 使用笔记
  9. sqlserver200864位下载_sql2008r2企业版下载-sql2008r2安装包64位 最新版 - 极光下载站...
  10. Android通讯录程序设计报告,Android个人通讯录课程设计报告.doc