注意:前置知识: HTTP: https://xingzhu.top/archives/web-fu-wu-qi Linux 多线程: https://xingzhu.top/archives/duo-xian-cheng
源码放github
上了,欢迎star
: https://github.com/xingzhuz/webServer
思路
实现代码 server.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #pragma once #include <arpa/inet.h> #include <sys/epoll.h> #include <stdio.h> #include <fcntl.h> #include <errno.h> #include <sys/stat.h> #include <assert.h> #include <sys/sendfile.h> #include <dirent.h> #include <string.h> #include <strings.h> #include <unistd.h> #include <stdlib.h> #include <ctype.h> #include <pthread.h> struct FdInfo { int fd; int epfd; pthread_t tid; }; int initListenFd (unsigned short port) ;int epollRun (int lfd) ;void *acceptClient (void *arg) ;void *recvHttpRequest (void *arg) ;int parseRequestLine (const char *line, int cfd) ;int sendFile (const char *fileName, int cfd) ;int sendHeadMsg (int cfd, int status, const char *descr, const char *type, int length) ;const char *getFileType (const char *name) ;int sendDir (const char *dirName, int cfd) ;int hexToDec (char c) ;void decodeMsg (char *to, char *from) ;
main.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include "server.h" int main (int argc, char *argv[]) { if (argc < 3 ) { printf ("./a.out port path\n" ); return -1 ; } unsigned short port = atoi (argv[1 ]); chdir (argv[2 ]); int lfd = initListenFd (port); epollRun (lfd); return 0 ; }
initListenFd 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 int initListenFd (unsigned short port) { int lfd = socket (AF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror ("socket" ); return -1 ; } int opt = 1 ; int ret = setsockopt (lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt); if (ret == -1 ) { perror ("setsocket" ); return -1 ; } struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons (port); addr.sin_addr.s_addr = INADDR_ANY; ret = bind (lfd, (struct sockaddr *)&addr, sizeof addr); if (ret == -1 ) { perror ("bind" ); return -1 ; } ret = listen (lfd, 128 ); if (ret == -1 ) { perror ("listen" ); return -1 ; } return lfd; }
这些步骤都是基础的 Socket
网络通信部分,不再赘述
解释端口复用:因为存在服务器端主动断开连接的情况,如果是服务器端主动断开连接,主动断开的一方存在一个等待时长,也就是在这个等待时长内,端口还是没有被释放,时长结束后才会释放
如果不想等待这个时长或者由于这个时长而换端口,就需要设置这个端口复用,设置后即使即使是在等待时长时间段内,仍可使用该端口
1 int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen) ;
sockfd : 套接字的文件描述符,通常是通过 socket()
函数创建的
level : 选项所在的协议层,通常为 SOL_SOCKET
,表示通用的套接字选项。也可以是特定协议的层,例如 IPPROTO_TCP
optname : 需要设置的选项的名称。可以是多种选项,如:
SO_REUSEADDR
: 允许重用本地地址
SO_KEEPALIVE
: 启用 TCP 的保活机制
SO_BROADCAST
: 允许发送广播消息
optval : 指向要设置的选项值的指针。这个值的类型取决于选项的类型
optlen : optval
所指向的值的大小,通常使用 sizeof()
来获取
epollRun 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 int epollRun (int lfd) { int epfd = epoll_create (1 ); if (epfd == -1 ) { perror ("epoll_create" ); return -1 ; } struct epoll_event ev; ev.data.fd = lfd; ev.events = EPOLLIN; int ret = epoll_ctl (epfd, EPOLL_CTL_ADD, lfd, &ev); if (ret == -1 ) { perror ("epoll_ctl" ); return -1 ; } struct epoll_event evs[1024 ]; int size = sizeof (evs) / sizeof (struct epoll_event); while (1 ) { int num = epoll_wait (epfd, evs, size, -1 ); for (int i = 0 ; i < num; i++) { struct FdInfo *info = (struct FdInfo *)malloc (sizeof (struct FdInfo)); int fd = evs[i].data.fd; info->epfd = epfd; info->fd = fd; if (fd == lfd) { pthread_create (&info->tid, NULL , acceptClient, info); } else { pthread_create (&info->tid, NULL , recvHttpRequest, info); } } } }
epoll
是 IO 多路转接 / 复用中的一个实现,可以大大提高效率,IO 多路转接/复用可以实现一个线程就监视多个文件描述符,其实现机制是在内核去中监视的,也就是可以大大减小开销,不用手动创建线程阻塞等待连接了,内核区监视是否有连接请求
而 epoll
是实现方式中效率较高的,是基于红黑树实现的,搜索起来快速
acceptClient 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void *acceptClient (void *arg) { struct FdInfo *info = (struct FdInfo *)arg; int cfd = accept (info->fd, NULL , NULL ); if (cfd == -1 ) { perror ("accept" ); return NULL ; } int flag = fcntl (cfd, F_GETFL); flag |= O_NONBLOCK; fcntl (cfd, F_SETFL, flag); struct epoll_event ev; ev.data.fd = cfd; ev.events = EPOLLIN | EPOLLET; int ret = epoll_ctl (info->epfd, EPOLL_CTL_ADD, cfd, &ev); if (ret == -1 ) { perror ("epoll_ctl" ); return NULL ; } printf ("acceptClient threadId: %ld\n" , info->tid); free (info); return 0 ; }
epoll
工作模式中,边缘非阻塞模式效率最高,因此采用这个,所以设置了文件描述符为非阻塞模式(默认为阻塞)
这里的连接和接收数据用多线程处理效率更高,即使之前已经实现了多个客户端和多个服务器端通信
recvHttpRequest 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 void *recvHttpRequest (void *arg) { struct FdInfo *info = (struct FdInfo *)arg; int len = 0 , total = 0 ; char tmp[1024 ] = {0 }; char buf[4096 ] = {0 }; while ((len = recv (info->fd, tmp, sizeof tmp, 0 )) > 0 ) { if (total + len < sizeof buf) { memcpy (buf + total, tmp, len); } total += len; } if (len == -1 && errno == EAGAIN) { char *pt = strstr (buf, "\r\n" ); int reLen = pt - buf; buf[reLen] = '\0' ; parseRequestLine (buf, info->fd); } else if (len == 0 ) { epoll_ctl (info->epfd, EPOLL_CTL_DEL, info->fd, NULL ); close (info->fd); } else perror ("recv" ); printf ("resvMsg threadId: %ld\n" , info->tid); free (info); return NULL ; }
上述 total
是偏移量,因为 memcpy
是从起始位置开始复制
虽然 buf
只有 4096 字节,存在读不完所有的请求数据,但是这也是没问题的,有用的数据 4096 已经够了,因为请求行最重要,只需要知道客户端向服务器请求的静态资源是什么,即便后面没读完,也不影响
由于这个套接字是非阻塞,所以当数据读完后,不阻塞,但是返回 -1,但是读取数据失败也是返回 -1,这就无法判断是否是读取完数据了,此时再用到 errno == EAGAIN
就能判断成功
如果套接字是阻塞的,当读取完数据后,会一直阻塞,所以书写逻辑需要更改,内部判断是否读取完毕,然后 break
循环
parseRequestLine 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 int parseRequestLine (const char *line, int cfd) { char method[12 ]; char path[1024 ]; sscanf (line, "%[^ ] %[^ ]" , method, path); if (strcasecmp (method, "get" ) != 0 ) { return -1 ; } decodeMsg (path, path); char *file = NULL ; if (strcmp (path, "/" ) == 0 ) { file = "./" ; } else { file = path + 1 ; } struct stat st; int ret = stat (file, &st); if (ret == -1 ) { sendHeadMsg (cfd, 404 , "Not Found" , getFileType (".html" ), -1 ); sendFile ("404.html" , cfd); return 0 ; } if (S_ISDIR (st.st_mode)) { sendHeadMsg (cfd, 200 , "OK" , getFileType (".html" ), -1 ); sendDir (file, cfd); } else { sendHeadMsg (cfd, 200 , "OK" , getFileType (file), st.st_size); sendFile (file, cfd); } }
sendFile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 int sendFile (const char *fileName, int cfd) { int fd = open (fileName, O_RDONLY); assert (fd > 0 ); #if 0 while (1 ) { char buf[1024 ]; int len = read (fd, buf, sizeof buf); if (len > 0 ) { send (cfd, buf, len, 0 ); usleep (10 ); } else if (len == 0 ) break ; else perror ("read" ); } #else off_t offset = 0 ; int size = lseek (fd, 0 , SEEK_END); lseek (fd, 0 , SEEK_SET); while (offset < size) { int ret = sendfile (cfd, fd, &offset, size - offset); printf ("ret value: %d\n" , ret); if (ret == -1 && errno != EAGAIN) { perror ("sendfile" ); } } #endif return 0 ; }
上述是发送文件的两种方式
第一种方式的 usleep(10)
很重要,发送数据很快,但是客户端读数据不一定这么快,客户端需要读取数据,然后进行解析,然后呈现出,这都需要耗时间的,不休眠一会儿,会存在接收数据不一致的问题(我遭受过…)
第二种方式使用库函数 sendfile
,通过这个函数发送,比手写的发送文件代码效率高,因为会减少拷贝次数,第四个参数是发送的大小,size - offset
的原因是 offset
这个参数是传入传出参数,会偏移到发送的位置,由于多次发送,前面发送了数据之后,就不是 size
了,就需要减去发送的字节数,也就是传出的偏移量 offset
注意 lseek
函数计算文件大小,会移动文件的指针,且 sendfile
也是有内部也是有缓存大小的,因此需要循环读取发送
if
判断是因为文件描述符改为了非阻塞模式,会一直读取数据,如果数据读完,也会返回 -1
,所以就需要再加个判断
sendHeadMsg 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int sendHeadMsg (int cfd, int status, const char *descr, const char *type, int length) { char buf[4096 ] = {0 }; sprintf (buf, "http/1.1 %d %s\r\n" , status, descr); sprintf (buf + strlen (buf), "content-type: %s\r\n" , type); sprintf (buf + strlen (buf), "content-length: %d\r\n\r\n" , length); send (cfd, buf, strlen (buf), 0 ); return 0 ; }
getFileType 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const char *getFileType (const char *name) { const char *dot = strrchr (name, '.' ); if (dot == NULL ) return "text/plain;charset=utf-8" ; if (strcmp (dot, ".html" ) == 0 || strcmp (dot, ".htm" ) == 0 ) return "text/html; charset=utf-8" ; if (strcmp (dot, ".jpg" ) == 0 || strcmp (dot, ".jpeg" ) == 0 ) return "image/jpeg" ; if (strcmp (dot, ".gif" ) == 0 ) return "image/gif" ; if (strcmp (dot, ".png" ) == 0 ) return "image/png" ; if (strcmp (dot, ".css" ) == 0 ) return "text/css" ; if (strcmp (dot, ".au" ) == 0 ) return "audio/basic" ; if (strcmp (dot, ".wav" ) == 0 ) return "audio/wav" ; if (strcmp (dot, ".mp3" )) return "audio/mp3" ; return "text/plain; charset = utf-8" ; }
sendDir 下述拼接是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <html > <head > <title > test</title > </head > <body > <table > <tr > <td > </td > <td > </td > </tr > <tr > <td > </td > <td > </td > </tr > </table > </body > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 int sendDir (const char *dirName, int cfd) { char buf[8192 ] = {0 }; sprintf (buf, "<html>" "<head>" "<title>%s</title>" "<style>" "body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; }" "h1 { color: #2c3e50; text-align: center; }" "table { width: 100%%; border-collapse: collapse; margin-top: 20px; }" "th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }" "th { background-color: #3498db; color: white; }" "tr:hover { background-color: #e7f3ff; }" "a { text-decoration: none; color: #3498db; transition: color 0.3s; }" "a:hover { color: #2980b9; text-decoration: underline; }" "</style>" "</head>" "<body><h1>%s</h1><table><tr><th>名称</th><th>大小 (字节)</th></tr>" , dirName, dirName); struct dirent **namelist ; int num = scandir(dirName, &namelist, NULL , alphasort); for (int i = 0 ; i < num; i++) { char *name = namelist[i]->d_name; struct stat st ; char subPath[1024 ] = {0 }; sprintf (subPath, "%s/%s" , dirName, name); stat(subPath, &st); if (S_ISDIR(st.st_mode)) { sprintf (buf + strlen (buf), "<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>" , name, name, st.st_size); } else { sprintf (buf + strlen (buf), "<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>" , name, name, st.st_size); } send(cfd, buf, strlen (buf), 0 ); memset (buf, 0 , sizeof buf); free (namelist[i]); } sprintf (buf, "</table></body></html>" ); send(cfd, buf, strlen (buf), 0 ); free (namelist); return 0 ; }
拼接 html
网页元素,是因为需要一个网页形式发送给浏览器
可以拼一份,发一份,因为底层使用的 TCP
协议
注意上述 a
标签那儿 \"%s/\"
需要 \
转义,因为前面已经有 "
了,所以需要用 \
转义,%s
后面加 /
是因为可能需要点击进入这个子目录,所以必须要这个 /
注意: 中文乱码问题
HTTP 协议中,不支持特殊字符 (如中文),会自动转义为 utf-8
编码,也就是如果当前文件名为中文,那么 linux
会将这个特殊字符转换为 utf-8
编码
如 /Linux%E5%86%85%E6%A0%B8.jpg
原本是 /Linux内核.jpg
,这样之后发送信息时就打不开了,报错 Not Found
,因为本地文件名是带有中文,但是经过代码处理后,程序读出的文件名没有中文,就找不到了
因此需要转换一下
decodeMsg 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 int hexToDec (char c) { if (c >= '0' && c <= '9' ) return c - '0' ; if (c >= 'a' && c <= 'f' ) return c - 'a' + 10 ; if (c >= 'A' && c <= 'F' ) return c - 'A' + 10 ; return 0 ; } void decodeMsg (char *to, char *from) { for (; *from != '\0' ;) { if (from[0 ] == '%' && isxdigit (from[1 ]) && isxdigit (from[2 ])) { *to = hexToDec(from[1 ]) * 16 + hexToDec(from[2 ]); to++; from += 3 ; } else { *to = *from; to++; from++; } } *to = '\0' ; }
这个没必要理解,直接网上搜索即可,这里让 GPT 润色修改成功的
404.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 404 页面未找到</title > <style > body { font-family : Arial, sans-serif; background-color : #f4f4f4 ; margin : 0 ; padding : 0 ; display : flex; justify-content : center; align-items : center; height : 100vh ; text-align : center; } .container { background-color : #fff ; border-radius : 8px ; box-shadow : 0 2px 10px rgba (0 , 0 , 0 , 0.1 ); padding : 40px ; max-width : 400px ; width : 100% ; } h1 { font-size : 72px ; color : #e74c3c ; margin : 0 ; } h2 { color : #333 ; margin : 20px 0 ; } p { color : #666 ; margin-bottom : 20px ; } a { text-decoration : none; background-color : #3498db ; color : #fff ; padding : 10px 20px ; border-radius : 5px ; transition : background-color 0.3s ; } a :hover { background-color : #2980b9 ; } </style > </head > <body > <div class ="container" > <h1 > 404</h1 > <h2 > 页面未找到</h2 > <p > 抱歉,我们找不到您请求的页面。</p > <a href ="/" > 返回首页</a > </div > </body > </html >
效果展示 1 2 3 # 编译 gcc *.c -o server ./server
刚登上服务器页面
点击 code
文件目录中的 cal.c
代码文件
点击 html
目录中的一个 html
文件
点击 Image
目录中的一个图片
点击 mp3
文件,开始播放
如果是一个不存在的文件
说明:参考学习:https://subingwen.cn/
xingzhu
keep trying!keep doing!believe in yourself!
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 星竹 !