• 黑马-web服务器与http协议


    黑马-web服务器

    【回顾】两种网络通信模型:

    B/S:Browser/Server

    优点:1. 安全性好;2. 跨平台方便;3. 开发工作量小 (在线应用)

    缺点:1. 不能缓存大量数据;2. 必须严格遵守http协议

    C/S:Client/Server

    优点:1. 可以缓存大量数据(需要下载应用);2. 可以自定义协议, 协议选择灵活(腾讯);3. 速度快

    缺点:1. 安全性差;2. 开发工作量大;3. 跨平台难

    我们访问的互联网网站,是通过域名,域名被解析为公网ip地址和端口,从而实现本机的浏览器和主机通信。

    本文目标在本机实现一个简单的web服务器myhttpd。能够给本机浏览器提供服务,供用户借助浏览器访问主机中的文件

    html(超文本标记语言-网页语言)

    超文本即不仅限于页面上显示文字

    本地写一个html,用浏览器直接点开,是一件很容易的事情。往html框架里面填和在线调试即可。

    下面是一个404页面的html例子:

    页面上也可以有其他元素:列表&图片和超链接

    1. html>
    2. <html>
    3. <head>
    4. <title>A Demotitle>
    5. head>
    6. <body>
    7. <p id="top">
    8. <ul type="circle">
    9. <li>option1li>
    10. <li>option2li>
    11. <li>option3li>
    12. <li>option4li>
    13. ul>
    14. <ol type="A">
    15. <li>option1li>
    16. <li>option2li>
    17. <li>option3li>
    18. <li>option4li>
    19. ol>
    20. <img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" width="300"/>
    21. <img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" />
    22. <img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" />
    23. <img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" />
    24. <img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" />
    25. <a href="http://jd.com" target="_blank" title="去京东">请跳转至京东a>
    26. <a href="http://jd.com" target="_blank" title="去京东">
    27. <img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" width="100"/>
    28. a>
    29. <a href="#top">回到顶部a>
    30. body>
    31. html>
    这个是关键:现学现用网站

    w3school 在线教程

    菜鸟教程 - 学的不仅是技术,更是梦想!

    XML、js、ajax、cgi、asp

    js是脚本语言,让页面变活,使得用户和页面打交道

    cgi建立了服务器和网页之间的通信;

    浏览器和服务器的通信协议 http

    思考 B/S 机制 或者说 浏览器机制:

    当点击页面上的一个跳转链接/图片后,浏览器封装了客户的请求信息,把请求发送给对应服务器的端口,服务器收到请求做一个应答给浏览器,浏览器再把收到的数据解析出来。我们关注这个通信过程。

    应用层协议--http协议格式

    通常HTTP消息包括两种:客户机向服务器的请求 和 服务器向客户机的响应消息

    请求消息Request(浏览器发给服务器):
    http协议头
    1. 请求行:说明请求类型,要访问的资源以及使用的http版本
    2. 请求头:说明服务器要使用的附加信息
    3. 空行:必须有!即使没有请求数据。表示http协议头结束;如果是POST请求,传送数据在空行后面。【到这儿,http协议头结束。】
    4. 请求数据:也叫主体,可以添加任意的其他数据

    当点击页面上一个超链接get请求时,浏览器就会自动组织封装一个协议

    以下是浏览器发送给服务器的http协议头内容举例:

    1. //第一行 GET 类请求;要访问的资源以及使用的http版本。对客户来说 /就是根目录,其他访问不到
    2. GET /hello.c HTTP/1.1
    3. //协议头/请求头
    4. Host:localhost:2222
    5. User-Agent:Mozilla/5.0(X11;Ubuntu;Linux i686;rv:24.0)Gecko/201001 01 Firefox/24.0
    6. Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    7. Accept-Language:zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
    8. Accept-Encoding:gzip,deflate
    9. Connection:keep-alive
    10. If-Modified-Since:Fri,18 Jul 2014 08:36:36 GMT
    11. //空行
    12. \r\n
    http请求类型 

    响应消息(服务器发给浏览器):
    http应答协议消息头
    1. 状态行:包括http协议版本号,状态码,状态信息
    2. 消息报头:说明客户端要使用的一些附加信息(有些行非必要,可以不回发)
    3. 空行:必须!【消息头】
    4. 响应正文:服务器返回给客户端的文本信息(或数据流)
    1. //包括http协议版本号,状态码,状态信息
    2. HTTP/1.1 200 OK
    3. //消息报头:说明客户端要使用的一些附加信息
    4. Server:xhttpd
    5. Date:Fri,18 Jul 2014 14:34:26 GMT
    6. Content-Type:text/plain;charset=iso-8859-1 //请求回来的数据,包含数据类型和编码格式。这样回来的数据就能以不同形式显示
    7. Content-Length:32 //浏览器读数据的长度按照这个来。因此,要么不写,数据回来浏览器自己算;要么就写对
    8. Content-Language:zh-CN
    9. Last-Modified:Fri,18,Jul 2014 08:36:36 GMT
    10. Connection:close //http默认,浏览器和服务器的通信模式一般都是,请求一次,应答一次,然后就断开。
    11. //空行
    12. \r\n
    13. //响应正文
    14. .....

    关于状态行状态码/标号 常见解释: 

    可以查一个http协议消息头吗?

    另外看下http;协议格式

    http协议学习网站

    web服务器的 epoll-http-server 实现

    • 选择合适的模型(epoll、libevent)监听客户链接和处理读数据请求

    1. //main.c
    2. #include
    3. #include
    4. #include
    5. #include "epoll_server.h"
    6. int main(int argc, const char* argv[])
    7. {
    8. if(argc < 3)
    9. {
    10. printf("eg: ./a.out port path\n");
    11. exit(1);
    12. }
    13. // 采用指定的端口
    14. int port = atoi(argv[1]);
    15. // 修改进程工作目录, 方便后续操作
    16. int ret = chdir(argv[2]);
    17. if(ret == -1)
    18. {
    19. perror("chdir error");
    20. exit(1);
    21. }
    22. // 启动epoll模型
    23. epoll_run(port);
    24. return 0;
    25. }
    • epoll_run(),模型起来之后分两条路,监听客户链接do_accept(lfd, epfd)
    1. void epoll_run(int port)
    2. {
    3. int i = 0;
    4. // 创建一个epoll树的根节点
    5. int epfd = epoll_create(MAXSIZE);
    6. if(epfd == -1) {
    7. perror("epoll_create error");
    8. exit(1);
    9. }
    10. // 添加要监听的节点
    11. // 先添加监听lfd
    12. int lfd = init_listen_fd(port, epfd);
    13. // 委托内核检测添加到树上的节点
    14. struct epoll_event all[MAXSIZE];
    15. while(1) {
    16. int ret = epoll_wait(epfd, all, MAXSIZE, 0);
    17. if(ret == -1) {
    18. perror("epoll_wait error");
    19. exit(1);
    20. }
    21. // 遍历发生变化的节点
    22. for(i=0; i
    23. {
    24. // 只处理读事件, 其他事件默认不处理
    25. struct epoll_event *pev = &all[i];
    26. if(!(pev->events & EPOLLIN)) {
    27. // 不是读事件
    28. continue;
    29. }
    30. if(pev->data.fd == lfd){
    31. // 接受连接请求
    32. do_accept(lfd, epfd);
    33. } else {
    34. // 读数据
    35. printf("======================before do read, ret = %d\n", ret);
    36. do_read(pev->data.fd, epfd);
    37. printf("=========================================after do read\n");
    38. }
    39. }
    40. }
    41. }
    42. int init_listen_fd(int port, int epfd)
    43. {
    44. // 创建监听的套接字
    45. int lfd = socket(AF_INET, SOCK_STREAM, 0);
    46. if(lfd == -1) {
    47. perror("socket error");
    48. exit(1);
    49. }
    50. // lfd绑定本地IP和port
    51. struct sockaddr_in serv;
    52. memset(&serv, 0, sizeof(serv));
    53. serv.sin_family = AF_INET;
    54. serv.sin_port = htons(port);
    55. serv.sin_addr.s_addr = htonl(INADDR_ANY);
    56. // 端口复用
    57. int flag = 1;
    58. setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    59. int ret = bind(lfd, (struct sockaddr*)&serv, sizeof(serv));
    60. if(ret == -1) {
    61. perror("bind error");
    62. exit(1);
    63. }
    64. // 设置监听
    65. ret = listen(lfd, 64);
    66. if(ret == -1) {
    67. perror("listen error");
    68. exit(1);
    69. }
    70. // lfd添加到epoll树上
    71. struct epoll_event ev;
    72. ev.events = EPOLLIN;
    73. ev.data.fd = lfd;
    74. ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    75. if(ret == -1) {
    76. perror("epoll_ctl add lfd error");
    77. exit(1);
    78. }
    79. return lfd;
    80. }
    81. // 接受新连接处理
    82. void do_accept(int lfd, int epfd)
    83. {
    84. struct sockaddr_in client;
    85. socklen_t len = sizeof(client);
    86. int cfd = accept(lfd, (struct sockaddr*)&client, &len);
    87. if(cfd == -1) {
    88. perror("accept error");
    89. exit(1);
    90. }
    91. // 打印客户端信息
    92. char ip[64] = {0};
    93. printf("New Client IP: %s, Port: %d, cfd = %d\n",
    94. inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)),
    95. ntohs(client.sin_port), cfd);
    96. // 设置cfd为非阻塞
    97. int flag = fcntl(cfd, F_GETFL);
    98. flag |= O_NONBLOCK;
    99. fcntl(cfd, F_SETFL, flag);
    100. // 得到的新节点挂到epoll树上
    101. struct epoll_event ev;
    102. ev.data.fd = cfd;
    103. // 边沿非阻塞模式
    104. ev.events = EPOLLIN | EPOLLET;
    105. int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    106. if(ret == -1) {
    107. perror("epoll_ctl add cfd error");
    108. exit(1);
    109. }
    110. }
    • 和处理http读数据请求 do_read(pev->data.fd, epfd)。这是不同业务代码不同的地方。下面重点写do_read函数 【这里先写处理来读单文件的业务】
    1. // 读数据
    2. void do_read(int cfd, int epfd)
    3. {
    4. // 将浏览器发过来的数据, 读到buf中
    5. char line[1024] = {0};
    6. // 读请求行
    7. int len = get_line(cfd, line, sizeof(line));
    8. if(len == 0) {
    9. printf("客户端断开了连接...\n");
    10. // 关闭套接字, cfd从epoll上del
    11. disconnect(cfd, epfd);
    12. } else {
    13. printf("============= 请求头 ============\n");
    14. printf("请求行数据: %s", line);
    15. // 还有数据没读完,继续读走。别放缓冲区,影响读后续新的数据
    16. while (1) {
    17. char buf[1024] = {0};
    18. len = get_line(cfd, buf, sizeof(buf));
    19. if (buf[0] == '\n') {
    20. break;
    21. } else if (len == -1)
    22. break;
    23. }
    24. printf("============= The End ============\n");
    25. }
    26. // 判断get请求
    27. if(strncasecmp("get", line, 3) == 0) { // 请求行: get /hello.c http/1.1
    28. // 处理http请求
    29. http_request(line, cfd);
    30. // 关闭套接字, cfd从epoll上del
    31. disconnect(cfd, epfd);
    32. }
    33. }
    • 监听到读事件,按照需要,我们只需要 get_line()获取http协议的第一行,其他消息头内容不重要。操作句柄是cfd。(由于http协议的数据每一行的结尾是 /r/n,因此读一行要针对这个特点。)
    1. // 可解析http请求消息的每一行内容,调用一次,读一行。
    2. //读 行以/r/n结尾的http消息头 的第一行
    3. int get_line(int sock, char *buf, int size)
    4. {
    5. int i = 0;
    6. char c = '\0';
    7. int n;
    8. while ((i < size - 1) && (c != '\n')) {
    9. n = recv(sock, &c, 1, 0);
    10. if (n > 0) {
    11. if (c == '\r') {
    12. n = recv(sock, &c, 1, MSG_PEEK);//MSG_PEEK模拟读一行,试探一下,下两个字符是不是/r/n,有可能只是/r,就不是结束符
    13. if ((n > 0) && (c == '\n')) {
    14. recv(sock, &c, 1, 0);
    15. } else {
    16. c = '\n';
    17. }
    18. }
    19. buf[i] = c;
    20. i++;
    21. } else {
    22. c = '\n';
    23. }
    24. }
    25. buf[i] = '\0';
    26. return i;
    27. }
    • 从首行中拆分GET,文件名,协议版本。获取用户请求的文件名,判断文件是否存在并获取文件属性,用stat()函数。
    1. // 断开连接的函数
    2. void disconnect(int cfd, int epfd)
    3. {
    4. int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
    5. if(ret == -1) {
    6. perror("epoll_ctl del cfd error");
    7. exit(1);
    8. }
    9. close(cfd);
    10. }
    11. // http请求处理
    12. void http_request(const char* request, int cfd)
    13. {
    14. // 拆分http请求行
    15. char method[12], path[1024], protocol[12];
    16. //使用正则表达式的方法,从一行字符串中,拆分出三个字符串(行中他们以空格隔开)
    17. sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
    18. printf("method = %s, path = %s, protocol = %s\n", method, path, protocol);
    19. // 转码 将不能识别的中文乱码 -> 中文
    20. // 解码 %23 %34 %5f
    21. decode_str(path, path);
    22. char* file = path+1; // 去掉path中的/ 获取访问文件名
    23. // 如果没有指定访问的资源, path就是“/”,因此下面返回数据就是默认显示资源目录中的内容(目录项)
    24. if(strcmp(path, "/") == 0) {
    25. // file的值, 资源目录的当前位置
    26. file = "./";
    27. }
    28. // 获取文件属性
    29. struct stat st;
    30. int ret = stat(file, &st);
    31. if(ret == -1) {
    32. send_error(cfd, 404, "Not Found", "NO such file or direntry");
    33. return;
    34. }
    35. // 判断是目录还是文件
    36. if(S_ISDIR(st.st_mode)) { // 目录
    37. // 发送头信息
    38. send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1);
    39. // 发送目录信息
    40. send_dir(cfd, file);
    41. } else if(S_ISREG(st.st_mode)) { // 文件
    42. // 发送消息报头
    43. send_respond_head(cfd, 200, "OK", get_file_type(file), st.st_size);
    44. // 发送文件内容
    45. send_file(cfd, file);
    46. }
    47. }

    正则表达式-脚本之家

    • 回发消息头字段中包括文件类型,定义个函数
    1. // 通过文件名获取文件的类型
    2. const char *get_file_type(const char *name)
    3. {
    4. char* dot;
    5. // 自右向左查找‘.’字符, 如不存在返回NULL
    6. dot = strrchr(name, '.');
    7. if (dot == NULL)
    8. return "text/plain; charset=utf-8";
    9. if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
    10. return "text/html; charset=utf-8";
    11. if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
    12. return "image/jpeg";
    13. if (strcmp(dot, ".gif") == 0)
    14. return "image/gif";
    15. if (strcmp(dot, ".png") == 0)
    16. return "image/png";
    17. if (strcmp(dot, ".css") == 0)
    18. return "text/css";
    19. if (strcmp(dot, ".au") == 0)
    20. return "audio/basic";
    21. if (strcmp( dot, ".wav" ) == 0)
    22. return "audio/wav";
    23. if (strcmp(dot, ".avi") == 0)
    24. return "video/x-msvideo";
    25. if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
    26. return "video/quicktime";
    27. if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
    28. return "video/mpeg";
    29. if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
    30. return "model/vrml";
    31. if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
    32. return "audio/midi";
    33. if (strcmp(dot, ".mp3") == 0)
    34. return "audio/mpeg";
    35. if (strcmp(dot, ".ogg") == 0)
    36. return "application/ogg";
    37. if (strcmp(dot, ".pac") == 0)
    38. return "application/x-ns-proxy-autoconfig";
    39. return "text/plain; charset=utf-8";
    40. }
    • 回发文件数据之前,先发应答消息头。
    1. // 发送响应头
    2. //消息头中有些参数是要外面传的,状态行的状态码 no,状态信息;消息报头的文件类型,长度等
    3. void send_respond_head(int cfd, int no, const char* desp, const char* type, long len)
    4. {
    5. char buf[1024] = {0};
    6. // 状态行
    7. sprintf(buf, "http/1.1 %d %s\r\n", no, desp);
    8. //send函数回发到服务器对应客户端的cfd
    9. send(cfd, buf, strlen(buf), 0);
    10. // 消息报头
    11. sprintf(buf, "Content-Type:%s\r\n", type);
    12. sprintf(buf+strlen(buf), "Content-Length:%ld\r\n", len);
    13. send(cfd, buf, strlen(buf), 0);
    14. // 空行
    15. send(cfd, "\r\n", 2, 0);
    16. }
    • 如果文件存在:open()打开,read()内容,写回给浏览器
    1. // 发送文件
    2. void send_file(int cfd, const char* filename)
    3. {
    4. // 打开文件
    5. int fd = open(filename, O_RDONLY);
    6. if(fd == -1) {
    7. send_error(cfd, 404, "Not Found", "NO such file or direntry");
    8. exit(1);
    9. }
    10. // 循环读文件
    11. char buf[4096] = {0};
    12. int len = 0, ret = 0;
    13. while( (len = read(fd, buf, sizeof(buf))) > 0 ) {
    14. // 发送读出的数据
    15. ret = send(cfd, buf, len, 0);
    16. if (ret == -1) {
    17. if (errno == EAGAIN) {
    18. perror("send error:");
    19. continue;
    20. } else if (errno == EINTR) {
    21. perror("send error:");
    22. continue;
    23. } else {
    24. perror("send error:");
    25. exit(1);
    26. }
    27. }
    28. }
    29. if(len == -1) {
    30. perror("read file error");
    31. exit(1);
    32. }
    33. close(fd);
    34. }

    至此,基本功能就写完了,可以基本测试一下。下面继续完善功能。

    • 考虑如果请求失败(文件不存在/发送失败等),服务器发送一个错误页面。即发送一个html类型的文本文件给浏览器。
    1. void send_error(int cfd, int status, char *title, char *text)
    2. {
    3. char buf[4096] = {0};
    4. sprintf(buf, "%s %d %s\r\n", "HTTP/1.1", status, title);
    5. sprintf(buf+strlen(buf), "Content-Type:%s\r\n", "text/html");
    6. sprintf(buf+strlen(buf), "Content-Length:%d\r\n", -1);
    7. sprintf(buf+strlen(buf), "Connection: close\r\n");
    8. send(cfd, buf, strlen(buf), 0);
    9. send(cfd, "\r\n", 2, 0);
    10. memset(buf, 0, sizeof(buf));
    11. sprintf(buf, "%d %s\n", status, title);
    12. sprintf(buf+strlen(buf), "

      %d %s

      \n"
      , status, title);
    13. sprintf(buf+strlen(buf), "%s\n", text);
    14. sprintf(buf+strlen(buf), "
      \n\n\n"
      );
    15. send(cfd, buf, strlen(buf), 0);
    16. return ;
    17. }

    注意一点(浏览器知识):

    • 如果判断客户端请求的没有指定文件,就要把服务器提供的目录返回出来【简单看看】

    本质上还是打开目录,读目录项,将目录项挨个写进html文件,再返回到浏览器,呈现出一个html页面。

    1. // 发送目录内容
    2. void send_dir(int cfd, const char* dirname)
    3. {
    4. int i, ret;
    5. // 拼一个html页面
    6. char buf[4094] = {0};
    7. sprintf(buf, "目录名: %s", dirname);
    8. sprintf(buf+strlen(buf), "

      当前目录: %s

      ", dirname);
    9. char enstr[1024] = {0};
    10. char path[1024] = {0};
    11. // 目录项二级指针
    12. struct dirent** ptr;
    13. int num = scandir(dirname, &ptr, NULL, alphasort);
    14. // 遍历
    15. for(i = 0; i < num; ++i) {
    16. char* name = ptr[i]->d_name;
    17. // 拼接文件的完整路径
    18. sprintf(path, "%s/%s", dirname, name);
    19. printf("path = %s ===================\n", path);
    20. struct stat st;
    21. stat(path, &st);
    22. // 编码生成 %E5 %A7 之类的东西
    23. encode_str(enstr, sizeof(enstr), name);
    24. // 如果是文件
    25. if(S_ISREG(st.st_mode)) {
    26. sprintf(buf+strlen(buf),
    27. "
    28. ",
    29. enstr, name, (long)st.st_size);
    30. } else if(S_ISDIR(st.st_mode)) { // 如果是目录
    31. sprintf(buf+strlen(buf),
    32. "
    33. ",
    34. enstr, name, (long)st.st_size);
    35. }
    36. ret = send(cfd, buf, strlen(buf), 0);
    37. if (ret == -1) {
    38. if (errno == EAGAIN) {
    39. perror("send error:");
    40. continue;
    41. } else if (errno == EINTR) {
    42. perror("send error:");
    43. continue;
    44. } else {
    45. perror("send error:");
    46. exit(1);
    47. }
    48. }
    49. memset(buf, 0, sizeof(buf));
    50. // 字符串拼接
    51. }
    52. sprintf(buf+strlen(buf), "
    53. %s%ld
      %s/%ld
      "
      );
    54. send(cfd, buf, strlen(buf), 0);
    55. printf("dir message send OK!!!!\n");
    • 汉字字符编码和解码

            每一个汉字在浏览器URL栏中会被转码成Unicode码进行显示和往服务器传输。因此,在URL栏访问带有汉字的文件时,服务器要先进行解码操作,解码称为方块字,才能在在linux文件目录下找到对应的文件;相应的,应该在服务器回发数据给浏览器时进行编码操作(请求目录时候会用到)。

    1. // 16进制数转化为10进制
    2. int hexit(char c)
    3. {
    4. if (c >= '0' && c <= '9')
    5. return c - '0';
    6. if (c >= 'a' && c <= 'f')
    7. return c - 'a' + 10;
    8. if (c >= 'A' && c <= 'F')
    9. return c - 'A' + 10;
    10. return 0;
    11. }
    12. /*
    13. * 这里的内容是处理%20之类的东西!是"解码"过程。
    14. * %20 URL编码中的‘ ’(space)
    15. * %21 '!' %22 '"' %23 '#' %24 '$'
    16. * %25 '%' %26 '&' %27 ''' %28 '('......
    17. * 相关知识html中的‘ ’(space)是 
    18. */
    19. void encode_str(char* to, int tosize, const char* from)
    20. {
    21. int tolen;
    22. for (tolen = 0; *from != '\0' && tolen + 4 < tosize; ++from) {
    23. if (isalnum(*from) || strchr("/_.-~", *from) != (char*)0) {
    24. *to = *from;
    25. ++to;
    26. ++tolen;
    27. } else {
    28. sprintf(to, "%%%02x", (int) *from & 0xff);
    29. to += 3;
    30. tolen += 3;
    31. }
    32. }
    33. *to = '\0';
    34. }
    35. void decode_str(char *to, char *from)
    36. {
    37. for ( ; *from != '\0'; ++to, ++from ) {
    38. if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) {
    39. *to = hexit(from[1])*16 + hexit(from[2]);
    40. from += 2;
    41. } else {
    42. *to = *from;
    43. }
    44. }
    45. *to = '\0';
    46. }
    调试遇到问题:

    现象:在ubuntu的火狐浏览器 URL栏输入本地服务器的 ip:端口/文件名 之后,文本文件可以正常访问,但是图片和音频文件返回的是乱码。

    定位:怀疑是返回数据没有指定对文件类型,代码中加打印,发现没问题。

    解决:尝试修改火狐浏览器的 编码,没找到修改位置。改用谷歌浏览器,显示图片和播放音频正常。说明服务器代码没问题。

    telnet调试

            也可使用telnet命令,借助IP和port,模拟浏览器行为(与命令行中的服务器通信),在终端中对访问的服务器进行调试,方便查看服务器会发给浏览器的http协议数据:

    1. $ telnet 127.0.0.1 9527
    2. GET /hello.c http/1.1

    此时在终端中可查看到服务器回发给浏览器的http应答协议及数据内容,可根据该信息进行调试。

    源码

    epoll_server.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #include "epoll_server.h"
    14. #define MAXSIZE 2000
    15. void send_error(int cfd, int status, char *title, char *text)
    16. {
    17. char buf[4096] = {0};
    18. sprintf(buf, "%s %d %s\r\n", "HTTP/1.1", status, title);
    19. sprintf(buf+strlen(buf), "Content-Type:%s\r\n", "text/html");
    20. sprintf(buf+strlen(buf), "Content-Length:%d\r\n", -1);
    21. sprintf(buf+strlen(buf), "Connection: close\r\n");
    22. send(cfd, buf, strlen(buf), 0);
    23. send(cfd, "\r\n", 2, 0);
    24. memset(buf, 0, sizeof(buf));
    25. sprintf(buf, "%d %s\n", status, title);
    26. sprintf(buf+strlen(buf), "

      %d %s

      \n"
      , status, title);
    27. sprintf(buf+strlen(buf), "%s\n", text);
    28. sprintf(buf+strlen(buf), "
      \n\n\n"
      );
    29. send(cfd, buf, strlen(buf), 0);
    30. return ;
    31. }
    32. void epoll_run(int port)
    33. {
    34. int i = 0;
    35. // 创建一个epoll树的根节点
    36. int epfd = epoll_create(MAXSIZE);
    37. if(epfd == -1) {
    38. perror("epoll_create error");
    39. exit(1);
    40. }
    41. // 添加要监听的节点
    42. // 先添加监听lfd
    43. int lfd = init_listen_fd(port, epfd);
    44. // 委托内核检测添加到树上的节点
    45. struct epoll_event all[MAXSIZE];
    46. while(1) {
    47. int ret = epoll_wait(epfd, all, MAXSIZE, 0);
    48. if(ret == -1) {
    49. perror("epoll_wait error");
    50. exit(1);
    51. }
    52. // 遍历发生变化的节点
    53. for(i=0; i
    54. {
    55. // 只处理读事件, 其他事件默认不处理
    56. struct epoll_event *pev = &all[i];
    57. if(!(pev->events & EPOLLIN)) {
    58. // 不是读事件
    59. continue;
    60. }
    61. if(pev->data.fd == lfd){
    62. // 接受连接请求
    63. do_accept(lfd, epfd);
    64. } else {
    65. // 读数据
    66. printf("======================before do read, ret = %d\n", ret);
    67. do_read(pev->data.fd, epfd);
    68. printf("=========================================after do read\n");
    69. }
    70. }
    71. }
    72. }
    73. // 读数据
    74. void do_read(int cfd, int epfd)
    75. {
    76. // 将浏览器发过来的数据, 读到buf中
    77. char line[1024] = {0};
    78. // 读请求行
    79. int len = get_line(cfd, line, sizeof(line));
    80. if(len == 0) {
    81. printf("客户端断开了连接...\n");
    82. // 关闭套接字, cfd从epoll上del
    83. disconnect(cfd, epfd);
    84. } else {
    85. printf("============= 请求头 ============\n");
    86. printf("请求行数据: %s", line);
    87. // 还有数据没读完,继续读走
    88. while (1) {
    89. char buf[1024] = {0};
    90. len = get_line(cfd, buf, sizeof(buf));
    91. if (buf[0] == '\n') {
    92. break;
    93. } else if (len == -1)
    94. break;
    95. }
    96. printf("============= The End ============\n");
    97. }
    98. // 判断get请求
    99. if(strncasecmp("get", line, 3) == 0) { // 请求行: get /hello.c http/1.1
    100. // 处理http请求
    101. http_request(line, cfd);
    102. // 关闭套接字, cfd从epoll上del
    103. disconnect(cfd, epfd);
    104. }
    105. }
    106. // 断开连接的函数
    107. void disconnect(int cfd, int epfd)
    108. {
    109. int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
    110. if(ret == -1) {
    111. perror("epoll_ctl del cfd error");
    112. exit(1);
    113. }
    114. close(cfd);
    115. }
    116. // http请求处理
    117. void http_request(const char* request, int cfd)
    118. {
    119. // 拆分http请求行
    120. char method[12], path[1024], protocol[12];
    121. sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
    122. printf("method = %s, path = %s, protocol = %s\n", method, path, protocol);
    123. // 转码 将不能识别的中文乱码 -> 中文
    124. // 解码 %23 %34 %5f
    125. decode_str(path, path);
    126. char* file = path+1; // 去掉path中的/ 获取访问文件名
    127. // 如果没有指定访问的资源, 默认显示资源目录中的内容
    128. if(strcmp(path, "/") == 0) {
    129. // file的值, 资源目录的当前位置
    130. file = "./";
    131. }
    132. // 获取文件属性
    133. struct stat st;
    134. int ret = stat(file, &st);
    135. if(ret == -1) {
    136. send_error(cfd, 404, "Not Found", "NO such file or direntry");
    137. return;
    138. }
    139. // 判断是目录还是文件
    140. if(S_ISDIR(st.st_mode)) { // 目录
    141. // 发送头信息
    142. send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1);
    143. // 发送目录信息
    144. send_dir(cfd, file);
    145. } else if(S_ISREG(st.st_mode)) { // 文件
    146. // 发送消息报头
    147. send_respond_head(cfd, 200, "OK", get_file_type(file), st.st_size);
    148. // 发送文件内容
    149. send_file(cfd, file);
    150. }
    151. }
    152. // 发送目录内容
    153. void send_dir(int cfd, const char* dirname)
    154. {
    155. int i, ret;
    156. // 拼一个html页面
    157. char buf[4094] = {0};
    158. sprintf(buf, "目录名: %s", dirname);
    159. sprintf(buf+strlen(buf), "

      当前目录: %s

      ", dirname);
    160. char enstr[1024] = {0};
    161. char path[1024] = {0};
    162. // 目录项二级指针
    163. struct dirent** ptr;
    164. int num = scandir(dirname, &ptr, NULL, alphasort);
    165. // 遍历
    166. for(i = 0; i < num; ++i) {
    167. char* name = ptr[i]->d_name;
    168. // 拼接文件的完整路径
    169. sprintf(path, "%s/%s", dirname, name);
    170. printf("path = %s ===================\n", path);
    171. struct stat st;
    172. stat(path, &st);
    173. // 编码生成 %E5 %A7 之类的东西
    174. encode_str(enstr, sizeof(enstr), name);
    175. // 如果是文件
    176. if(S_ISREG(st.st_mode)) {
    177. sprintf(buf+strlen(buf),
    178. "
    179. ",
    180. enstr, name, (long)st.st_size);
    181. } else if(S_ISDIR(st.st_mode)) { // 如果是目录
    182. sprintf(buf+strlen(buf),
    183. "
    184. ",
    185. enstr, name, (long)st.st_size);
    186. }
    187. ret = send(cfd, buf, strlen(buf), 0);
    188. if (ret == -1) {
    189. if (errno == EAGAIN) {
    190. perror("send error:");
    191. continue;
    192. } else if (errno == EINTR) {
    193. perror("send error:");
    194. continue;
    195. } else {
    196. perror("send error:");
    197. exit(1);
    198. }
    199. }
    200. memset(buf, 0, sizeof(buf));
    201. // 字符串拼接
    202. }
    203. sprintf(buf+strlen(buf), "
    204. %s%ld
      %s/%ld
      "
      );
    205. send(cfd, buf, strlen(buf), 0);
    206. printf("dir message send OK!!!!\n");
    207. #if 0
    208. // 打开目录
    209. DIR* dir = opendir(dirname);
    210. if(dir == NULL)
    211. {
    212. perror("opendir error");
    213. exit(1);
    214. }
    215. // 读目录
    216. struct dirent* ptr = NULL;
    217. while( (ptr = readdir(dir)) != NULL )
    218. {
    219. char* name = ptr->d_name;
    220. }
    221. closedir(dir);
    222. #endif
    223. }
    224. // 发送响应头
    225. void send_respond_head(int cfd, int no, const char* desp, const char* type, long len)
    226. {
    227. char buf[1024] = {0};
    228. // 状态行
    229. sprintf(buf, "http/1.1 %d %s\r\n", no, desp);
    230. send(cfd, buf, strlen(buf), 0);
    231. // 消息报头
    232. sprintf(buf, "Content-Type:%s\r\n", type);
    233. sprintf(buf+strlen(buf), "Content-Length:%ld\r\n", len);
    234. send(cfd, buf, strlen(buf), 0);
    235. // 空行
    236. send(cfd, "\r\n", 2, 0);
    237. }
    238. // 发送文件
    239. void send_file(int cfd, const char* filename)
    240. {
    241. // 打开文件
    242. int fd = open(filename, O_RDONLY);
    243. if(fd == -1) {
    244. send_error(cfd, 404, "Not Found", "NO such file or direntry");
    245. exit(1);
    246. }
    247. // 循环读文件
    248. char buf[4096] = {0};
    249. int len = 0, ret = 0;
    250. while( (len = read(fd, buf, sizeof(buf))) > 0 ) {
    251. // 发送读出的数据
    252. ret = send(cfd, buf, len, 0);
    253. if (ret == -1) {
    254. if (errno == EAGAIN) {
    255. perror("send error:");
    256. continue;
    257. } else if (errno == EINTR) {
    258. perror("send error:");
    259. continue;
    260. } else {
    261. perror("send error:");
    262. exit(1);
    263. }
    264. }
    265. }
    266. if(len == -1) {
    267. perror("read file error");
    268. exit(1);
    269. }
    270. close(fd);
    271. }
    272. // 解析http请求消息的每一行内容
    273. int get_line(int sock, char *buf, int size)
    274. {
    275. int i = 0;
    276. char c = '\0';
    277. int n;
    278. while ((i < size - 1) && (c != '\n')) {
    279. n = recv(sock, &c, 1, 0);
    280. if (n > 0) {
    281. if (c == '\r') {
    282. n = recv(sock, &c, 1, MSG_PEEK);
    283. if ((n > 0) && (c == '\n')) {
    284. recv(sock, &c, 1, 0);
    285. } else {
    286. c = '\n';
    287. }
    288. }
    289. buf[i] = c;
    290. i++;
    291. } else {
    292. c = '\n';
    293. }
    294. }
    295. buf[i] = '\0';
    296. return i;
    297. }
    298. // 接受新连接处理
    299. void do_accept(int lfd, int epfd)
    300. {
    301. struct sockaddr_in client;
    302. socklen_t len = sizeof(client);
    303. int cfd = accept(lfd, (struct sockaddr*)&client, &len);
    304. if(cfd == -1) {
    305. perror("accept error");
    306. exit(1);
    307. }
    308. // 打印客户端信息
    309. char ip[64] = {0};
    310. printf("New Client IP: %s, Port: %d, cfd = %d\n",
    311. inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)),
    312. ntohs(client.sin_port), cfd);
    313. // 设置cfd为非阻塞
    314. int flag = fcntl(cfd, F_GETFL);
    315. flag |= O_NONBLOCK;
    316. fcntl(cfd, F_SETFL, flag);
    317. // 得到的新节点挂到epoll树上
    318. struct epoll_event ev;
    319. ev.data.fd = cfd;
    320. // 边沿非阻塞模式
    321. ev.events = EPOLLIN | EPOLLET;
    322. int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    323. if(ret == -1) {
    324. perror("epoll_ctl add cfd error");
    325. exit(1);
    326. }
    327. }
    328. int init_listen_fd(int port, int epfd)
    329. {
    330. // 创建监听的套接字
    331. int lfd = socket(AF_INET, SOCK_STREAM, 0);
    332. if(lfd == -1) {
    333. perror("socket error");
    334. exit(1);
    335. }
    336. // lfd绑定本地IP和port
    337. struct sockaddr_in serv;
    338. memset(&serv, 0, sizeof(serv));
    339. serv.sin_family = AF_INET;
    340. serv.sin_port = htons(port);
    341. serv.sin_addr.s_addr = htonl(INADDR_ANY);
    342. // 端口复用
    343. int flag = 1;
    344. setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    345. int ret = bind(lfd, (struct sockaddr*)&serv, sizeof(serv));
    346. if(ret == -1) {
    347. perror("bind error");
    348. exit(1);
    349. }
    350. // 设置监听
    351. ret = listen(lfd, 64);
    352. if(ret == -1) {
    353. perror("listen error");
    354. exit(1);
    355. }
    356. // lfd添加到epoll树上
    357. struct epoll_event ev;
    358. ev.events = EPOLLIN;
    359. ev.data.fd = lfd;
    360. ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    361. if(ret == -1) {
    362. perror("epoll_ctl add lfd error");
    363. exit(1);
    364. }
    365. return lfd;
    366. }
    367. // 16进制数转化为10进制
    368. int hexit(char c)
    369. {
    370. if (c >= '0' && c <= '9')
    371. return c - '0';
    372. if (c >= 'a' && c <= 'f')
    373. return c - 'a' + 10;
    374. if (c >= 'A' && c <= 'F')
    375. return c - 'A' + 10;
    376. return 0;
    377. }
    378. /*
    379. * 这里的内容是处理%20之类的东西!是"解码"过程。
    380. * %20 URL编码中的‘ ’(space)
    381. * %21 '!' %22 '"' %23 '#' %24 '$'
    382. * %25 '%' %26 '&' %27 ''' %28 '('......
    383. * 相关知识html中的‘ ’(space)是 
    384. */
    385. void encode_str(char* to, int tosize, const char* from)
    386. {
    387. int tolen;
    388. for (tolen = 0; *from != '\0' && tolen + 4 < tosize; ++from) {
    389. if (isalnum(*from) || strchr("/_.-~", *from) != (char*)0) {
    390. *to = *from;
    391. ++to;
    392. ++tolen;
    393. } else {
    394. sprintf(to, "%%%02x", (int) *from & 0xff);
    395. to += 3;
    396. tolen += 3;
    397. }
    398. }
    399. *to = '\0';
    400. }
    401. void decode_str(char *to, char *from)
    402. {
    403. for ( ; *from != '\0'; ++to, ++from ) {
    404. if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) {
    405. *to = hexit(from[1])*16 + hexit(from[2]);
    406. from += 2;
    407. } else {
    408. *to = *from;
    409. }
    410. }
    411. *to = '\0';
    412. }
    413. // 通过文件名获取文件的类型
    414. const char *get_file_type(const char *name)
    415. {
    416. char* dot;
    417. // 自右向左查找‘.’字符, 如不存在返回NULL
    418. dot = strrchr(name, '.');
    419. if (dot == NULL)
    420. return "text/plain; charset=utf-8";
    421. if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
    422. return "text/html; charset=utf-8";
    423. if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
    424. return "image/jpeg";
    425. if (strcmp(dot, ".gif") == 0)
    426. return "image/gif";
    427. if (strcmp(dot, ".png") == 0)
    428. return "image/png";
    429. if (strcmp(dot, ".css") == 0)
    430. return "text/css";
    431. if (strcmp(dot, ".au") == 0)
    432. return "audio/basic";
    433. if (strcmp( dot, ".wav" ) == 0)
    434. return "audio/wav";
    435. if (strcmp(dot, ".avi") == 0)
    436. return "video/x-msvideo";
    437. if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
    438. return "video/quicktime";
    439. if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
    440. return "video/mpeg";
    441. if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
    442. return "model/vrml";
    443. if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
    444. return "audio/midi";
    445. if (strcmp(dot, ".mp3") == 0)
    446. return "audio/mpeg";
    447. if (strcmp(dot, ".ogg") == 0)
    448. return "application/ogg";
    449. if (strcmp(dot, ".pac") == 0)
    450. return "application/x-ns-proxy-autoconfig";
    451. return "text/plain; charset=utf-8";
    452. }

    epoll_server.h

    1. #ifndef _EPOLL_SERVER_H
    2. #define _EPOLL_SERVER_H
    3. int init_listen_fd(int port, int epfd);
    4. void epoll_run(int port);
    5. void do_accept(int lfd, int epfd);
    6. void do_read(int cfd, int epfd);
    7. int get_line(int sock, char *buf, int size);
    8. void disconnect(int cfd, int epfd);
    9. void http_request(const char* request, int cfd);
    10. void send_respond_head(int cfd, int no, const char* desp, const char* type, long len);
    11. void send_file(int cfd, const char* filename);
    12. void send_dir(int cfd, const char* dirname);
    13. void encode_str(char* to, int tosize, const char* from);
    14. void decode_str(char *to, char *from);
    15. const char *get_file_type(const char *name);
    16. #endif

    main.c

    1. #include
    2. #include
    3. #include
    4. #include "epoll_server.h"
    5. int main(int argc, const char* argv[])
    6. {
    7. if(argc < 3)
    8. {
    9. printf("eg: ./a.out port path\n");
    10. exit(1);
    11. }
    12. // 采用指定的端口
    13. int port = atoi(argv[1]);
    14. // 修改进程工作目录, 方便后续操作
    15. int ret = chdir(argv[2]);
    16. if(ret == -1)
    17. {
    18. perror("chdir error");
    19. exit(1);
    20. }
    21. // 启动epoll模型
    22. epoll_run(port);
    23. return 0;
    24. }

    makefile

    1. src = $(wildcard ./*.c)
    2. obj = $(patsubst ./%.c, ./%.o, $(src))
    3. myArgs= -Wall -g
    4. target=server
    5. CC=gcc
    6. ALL:$(target)
    7. $(target):$(obj)
    8. $(CC) $^ -o $@ $(myArgs)
    9. $(obj):%.o:%.c
    10. $(CC) -c $^ -o $@ $(myArgs)
    11. clean:
    12. -rm -rf $(obj) $(target)
    13. .PHONY: clean ALL

    回答一个经典问题:浏览器输入一个网址发生了什么? 

    在浏览器地址栏输入url到按下回车发生了什么?-CSDN博客

    在浏览器输入URL并回车后,会经历以下8个步骤:

    1. URL解析:浏览器会解析URL,将其分为协议、主机、端口、路径和查询参数等部分。
    2. DNS域名解析:浏览器会向DNS服务器发送请求,获取主机对应的IP地址。
    3. 建立TCP连接(三次握手):浏览器会向服务器发送SYN包,服务器回复ACK包和SYN包,浏览器再回复ACK包,完成三次握手。
    4. 发送HTTP请求:浏览器向服务器发送HTTP请求,请求中包含请求行、请求头、空行和请求数据等部分。
    5. 服务器处理相关的请求:服务器接收到请求后,会根据请求的内容进行处理,如请求一个页面,查询数据库、读取文件等。
    6. 返回响应的结果:服务器将处理后的结果封装成HTTP响应报文返回给浏览器,响应中包含状态行、响应头、空行和响应数据等部分。
    7. 关闭TCP连接(四次挥手):浏览器向服务器发送FIN包,服务器回复ACK包,服务器向浏览器发送FIN包,浏览器回复ACK包,完成四次挥手。
    8. HTML解析和页面渲染:浏览器接收到响应后,会对HTML进行解析,并根据CSS样式对页面进行渲染,最终呈现给用户。

    目前系统 QT端 和 Web端 的区别

    QT界面

    优点:

    运行稳定,可靠。有C++语法支持,开发友好。

    界面实际开发只需要一个人即可完成。

    目前本机QT端,直接从共享内存取出数据,不需要编码推流

    系统运行更加高效。

    支持更多的动画效果。

    缺点:

    依赖客户端,需要安装程序。

    远程只能通过远程桌面的方式,且同一个时间段只能一个用户操作。

    Web界面

    优点:

    不需要安装客户端。

    在浏览器上就可以访问系统。

    缺点:

    需要谷歌浏览器,连接外网。

    开发不支持C++,开发难度大。

    Web开发周期长,框架熟悉时间长。

    Web需要前段和后端,至少需要两个人同时开发,任务量比较大。

    需要从共享内存取出数据,编码,推流。受网络环境影响大,CPU编码对CPU的资源的消耗也大。

    从编码推流,web后端,web前端。涉及的环节较多,每个环节均会影响到了web显示。目前大家写的各个模块都没有经过长时间的测试,容易出bug的地方很多。

    目前web大系统的耦合度高,安全机制不健全,容易被攻击。

    目前系统 QT端 和 Web端 的区别 - 简书QT界面 优点: 运行稳定,可靠。有C++语法支持,开发友好。 界面实际开发只需要一个人即可完成。 目前本机QT端,直接从共享内存取出数据,不需要编码推流。 系统运行更加高效...icon-default.png?t=N7T8https://www.jianshu.com/p/7ae394e467b3

    路由器的管理页面192.168.1.1

    如何在计算机管理路由器,怎么查看路由器的管理IP地址?-CSDN博客

        想起来,路由器里面应该有一个web服务器,用户通过手机或者电脑连接自己新安装的路由器,输入默认的ip地址,192.168.1.1或者192.168.0.1(也是路由器内网地址),进入路由器的管理页面。

            开发时候,通过网线或者串口线,登录路由器页面;或者使用ipop工具,使用服务器远程登录协议 telnet或者ssh协议,登录到路由器的后台,进行路由器的升级,命令查询等操作。

            但是虽然能ping通,也要保证服务器的端口(telnet-23;ssh-22)是使能的。

    学习参考: 

    01-错误原因说明_哔哩哔哩_bilibili

    【精选】Linux网络编程学习笔记_bufferevent缺点_Daniel_187的博客-CSDN博客

    待看 

    7.01 常见的Web技术_哔哩哔哩_bilibili

  • 相关阅读:
    2023年中国研究生数学建模竞赛赛题浅析
    冒名顶替综合症:悄悄地杀死你的梦想
    苹果iOS群控系统的源代码分享!
    如何用servlet写注册登录页面?
    docker
    脚手架工程使用ElementUI
    计算机网络第三章习题
    爬山算法的详细介绍
    前端 Websocket + Stomp.js 的使用
    搭建自动化 Web 页面性能检测系统 —— 实现篇
  • 原文地址:https://blog.csdn.net/weixin_46697509/article/details/134260942