在实际开发中经常用到web框架,比如Servlet,SpringBoot等,这些开发框架提高了我们的开发效率,节省了开发时间。但是这会令我们技术人员处于浮云之上,看不到其本质。说实话,Java语言那么流行,其本质是运行在JRE虚拟机上的,而JRE是用C/C++语言开发的。与其说java跨平台,不如说是因为在不同平台上各自实现JRE,从而屏蔽了java语言直接与不同平台打交道。http协议广泛应用,也是基于TCP协议之上的封装。本节博主将带领大家用C语言在Linux环境下开发HTTP服务器,支持浏览器下载和浏览文件。另外还使用TCP协议开发了服务端和客户端来实现服务端监听客户端连接,然后向其发送一首唐诗。
目录
IP 地址本质是整数,但是为了方便,在使用的过程中都是用字符串来描述,超过两个字节的数据单元,在跨网络传输时候就需要考虑本地字节序和网络字节序的转换,Linux下主要使用api如下:
1)本地字节序转网络字节序
int inet_pton(int af, const char *src, void *dst);
参数:
af: 地址族协议,IPV4或者IPV6
AF_INET: IPV4 地址
AF_INET6: IPV6地址
src: 点分十进制的 ip 地址,例如192.168.1.2
dst: 传出参数,存放大端整形IP地址
返回值:成功返回 1,失败返回 0 或者 - 1
2)网络字节序转本机字节序
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数
af: 地址族协议,IPV4或者IPV6
AF_INET: IPV4 地址
AF_INET6: IPV6地址
src: 大端的整形 IP 地址
dst: 存储转换得到的小端的点分十进制的IP地址
size: dst内存中占用字节数
返回值:
成功:指针指向第三个参数对应的内存地址,通过返回值可以直接取出转换得到的IP字符串
失败: NULL
只能处理IPV4地址的api
(1)点分十进制IP转大端整形
in_addr_t inet_addr (const char *cp);
(2)大端整形转点分十进制IP
char* inet_ntoa(struct in_addr in);
1)主机字节序转网络字节序
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
2)网络字节序转主机字节序
uint16_t ntohs(uint16_t netshort)
uint32_t ntohl(uint32_t netlong);
int socket(int domain, int type, int protocol);
作用:创建socket文件描述符,通过该文件描述符可以操作内核中的某一块内存,用来进行网络通信
参数:
domain: 使用的地址族协议
AF_INET: IPv4 格式的 ip 地址
AF_INET6: IPv6 格式的 ip 地址
type:
SOCK_STREAM: 流式传输协议
SOCK_DGRAM: 报式 (报文) 传输协议
protocol: 一般写 0 ,表示使用默认的协议
SOCK_STREAM: 流式传输默认是 tcp
SOCK_DGRAM: 报式传输默认是udp
返回值:
成功:可用于套接字通信的文件描述符
失败: -1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:将文件描述符和本地的IP与端口进行绑定
参数:
sockfd: 监听的文件描述符,通过 socket () 调用得到
addr: 要绑定的 IP 和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序
addrlen: 参数 addr 指向的内存大小
返回值:成功返回 0,失败返回 - 1
作用:监听套接字
int listen(int sockfd, int backlog);
参数:
sockfd: 文件描述符,调用 socket () 得到
backlog: 同时能处理的最大连接,最大值为 128
返回值:成功返回 0,失败返回 -1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
作用:等待并接受客户端的连接请求, 建立新的连接, 得到一个通信的文件描述符,函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞。
当检测到有新的客户端连接时,解除阻塞,得到的描述符就可以和客户端通信。
参数:
sockfd: 监听的文件描述符
addr: 建立连接的客户端的地址信息
addrlen: addr 指向的内存大小
返回值:函数调用成功,得到一个文件描述符,调用失败返回 -1
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
参数:
sockfd: accept 或者connect函数的返回值
buf: 接收数据内存
size: 参数 buf 指向的内存的容量
flags: 一般不使用,指定为 0
返回值:
大于 0:实际接收的字节数
等于 0:对方断开了连接
-1:接收数据失败了
如果连接没有断开,接收不到数据,会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,时候就不会阻塞了,直接返回0。
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
参数:
fd: accept 或者connect函数的返回值
buf: 发送的数据
len: 要发送数据长度
flags: 一般不使用,指定为 0
返回值:
大于 0:实际发送的字节数,等于参数 len
-1:发送数据失败了
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:与服务器建立连接
参数:
sockfd: socket 返回值
addr: 要连接的服务器的 iP 和 端口,这个 IP 和端口都是大端的
addrlen: addr内存大小
返回值:连接成功返回 0,连接失败返回 - 1
http服务器实现建立在tcp协议之上,采用epoll和多线程的方式接收和处理客户端。解析从TCP传过来的数据,解析请求行,找到需要访问的资源。如果是目录则使用html的a标签href属性进行重定位,进入目录。如果是文件,txt和png文件浏览器在线预览,其他文件则让浏览器进行下载。
头文件:
- #pragma once
- #include
-
- //线程参数结构
- struct ThreadParam {
- pthread_t tid;
- int fd;
- int epfd;
- };
-
- int initListenFd(unsigned short port);
-
- int epoolRun(int lfd);
-
- void* acceptClient(void *);
-
- void* recvHttpRequest(void *);
-
- int parseRequestLine(int cfd, const char *line);
-
- int sendFile(int cfd, const char *fileName);
-
- int sendHeadMsg(int cfd, int status, const char *descr, const char *type, int length);
-
- const char *getFileType(const char *name);
-
- int sendDir(int cfd, const char*dirName);
-
- int hex2dec (char c);
-
- char dec2hex (short int c);
-
- void urlDecode(char* from, char *to);
源文件
- #include "HttpServer.h"
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- int initListenFd(unsigned short port) {
- int lfd = socket(AF_INET, SOCK_STREAM, 0);
- if (-1 == lfd) {
- perror("scoket");
- return -1;
- }
- int opt = -1;
- int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
- if (-1 == ret) {
- perror("setsockopt");
- 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, (const struct sockaddr *)&addr, sizeof(addr));
- if (-1 == ret) {
- perror("bind");
- return -1;
- }
- ret = listen(lfd, 128);
- if (-1 == ret) {
- perror("listen");
- return -1;
- }
- return lfd;
- }
-
- int epoolRun(int lfd) {
- int epfd = epoll_create(1);
- if (-1 == epfd) {
- 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 (-1 == ret) {
- perror("epoll_ctl");
- return -1;
- }
- struct epoll_event evs[2048];
- int maxwait = sizeof(evs) / sizeof(struct epoll_event);
- while (1) {
- int num = epoll_wait(epfd, evs, maxwait, -1);
- for (int i = 0; i <= num; i++) {
- int fd = evs[i].data.fd;
- pthread_t tid;
- struct ThreadParam *param =
- (struct ThreadParam *)malloc(sizeof(struct ThreadParam));
- param->fd = fd;
- param->epfd = epfd;
- param->tid = tid;
- if (fd == lfd) {
- pthread_create(&tid, NULL, acceptClient, param);
- pthread_detach(tid);
- }
- else {
- pthread_create(&tid, NULL, recvHttpRequest, param);
- pthread_detach(tid);
- }
- }
- }
-
- return 0;
- }
-
- void* acceptClient(void *arg) {
- struct ThreadParam *param = (struct ThreadParam *)arg;
- if (!param) {
- return NULL;
- }
- int lfd = param->fd;
- int epfd = param->epfd;
- int cfd = accept(lfd, NULL, NULL);
- if (-1 == cfd) {
- 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; //cfd边缘非阻塞模式,效率最高
- int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
- if (-1 == cfd) {
- perror("epoll_ctl");
- }
- free(param);
- param = NULL;
- printf("accept thread %ld\n", pthread_self());
- return NULL;
- }
-
- void* recvHttpRequest(void *arg) {
- struct ThreadParam *param = (struct ThreadParam *)arg;
- char buf[8192] = {0};
- int len = 0;
- int total = 0;
- char tmpBuf[1024] = {0};
- if (!param) {
- return NULL;
- }
- int cfd = param->fd;
- int epfd = param->epfd;
- while ((len = recv(cfd, tmpBuf, sizeof(tmpBuf), 0)) > 0) {
- if (total + len < sizeof(buf)) {
- memcpy(buf + total, tmpBuf, len);
- }
- total += len;
- }
- if (-1 == len && errno == EAGAIN && total > 0) { //接收数据完毕
- //解析http协议
- char *pt = strstr(buf, "\r\n");
- int reqLen = pt - buf;
- buf[reqLen] = '\0';
- parseRequestLine(cfd, buf);
- }
- else if (0 == len) { //客户端断开了连接
- epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
- close(cfd);
- }
- else {
- perror("recv");
- }
- free(param);
- param = NULL;
- printf("client thread %ld\n", pthread_self());
- return NULL;
- }
-
- int parseRequestLine(int cfd, const char *line) {
- char method[32] = {0};
- char path[2048] = {0};
- char decodePath[1024] = {0};
- char protocol[128] = {0};
- //sscanf解析格式化字符串
- sscanf(line, "%[^ ] %[^ ] %s", method, path, protocol);
- printf("method: %s, path: %s protocol: %s\n", method, path, protocol);
- if (0 != strcasecmp(method, "get")) {
- return -1;
- }
- urlDecode(path, decodePath);
- //http中/代表服务端工作的资源根目录
- char *file = NULL;
- if (0 == strcmp(decodePath, "/")) {
- file = ".";
- }
- else {
- file = decodePath + 1;
- }
- struct stat st;
- int ret = stat(file, &st);
- if (-1 == ret) {
- //回复404页面
- sendHeadMsg(cfd, 404, "Not Found", getFileType(".html"), -1); //-1表示不知道长度,让浏览器自己解析去
- sendFile(cfd, "404.html");
- return 0;
- }
- if (S_ISDIR(st.st_mode)) {
- sendHeadMsg(cfd, 200, "OK", getFileType(".html"), -1);
- sendDir(cfd, file);
- }
- else {
- sendHeadMsg(cfd, 200, "OK", getFileType(file), st.st_size);
- sendFile(cfd, file);
- }
-
- return 0;
- }
-
- int sendFile(int cfd, const char *fileName) {
- //读一部分数据,发送一部分数据,因为tcp是面向连接的流式的
- 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(20); //减轻接收端压力
- }
- else if (0 == len) {
- break;
- }
- else {
- perror("read");
- }
- }
- #endif
- #if 1
- off_t len = 0;
- int size = lseek(fd, 0, SEEK_END);
- lseek(fd, 0, SEEK_SET);
- while (len < size) {
- int ret = sendfile(cfd, fd, &len, size - len);
- printf("ret value %d \n", ret);
- if (-1 == ret) {
- if (EAGAIN == errno) {
- printf("no data\n");
- perror("sendfile");
- }
- else {
- printf("client quit \n");
- break;
- }
- }
-
- }
- #endif
- return 0;
- }
-
- int sendHeadMsg(int cfd, int status, const char *descr, const char *type, int length) {
- char buf[8192] = {0};
- int offset = 0;
- int ret = sprintf(buf + offset, "http/1.1 %d %s\r\n", status, descr);
- offset += ret;
- ret = sprintf(buf + offset, "content-type: %s\r\n", type);
- offset += ret;
- ret = sprintf(buf + offset, "content-length: %d\r\n\r\n", length);
- offset += ret;
- send(cfd, buf, offset, 0);
- return 0;
- }
-
- const char *getFileType(const char *name) {
- const char* dot = strrchr(name, '.');
- if (NULL == dot) {
- return "text/palin; charset=utf-8";
- }
- if (0 == strcasecmp(dot, ".html")) {
- return "text/html; charset=utf-8";
- }
- if (0 == strcasecmp(dot, ".png")) {
- return "image/png; charset=utf-8";
- }
- if (0 == strcasecmp(dot, ".txt")) {
- return "text/palin; charset=utf-8";
- }
- // ...
- return "application/octet-stream; charset=utf-8";
- }
-
- int sendDir(int cfd, const char*dirName) {
- char buf[2048] = {0};
- int len = 0;
- int ret = sprintf(buf + len, "
%s ", dirName);- len += ret;
- struct dirent** namelist = NULL;
- int num = scandir(dirName, &namelist, NULL, alphasort);
- for (int i = 0; i < num; i++) {
- char * name = namelist[i]->d_name;
- struct stat st;
- char path[1024] = {0};
- sprintf(path, "%s/%s", dirName, name);
- stat(path, &st);
- if (S_ISDIR(st.st_mode)) {
- if (!strcmp(".", name) || !strcmp("..", name)) {
- continue;
- }
- ret = sprintf(buf + len, "
%s %ld ", \ - name, name, st.st_size);
- }
- else {
- ret = sprintf(buf + len, "
%s %ld ", - name, name, st.st_size);
- }
- len += ret;
- send(cfd, buf, len, 0);
- len = 0;
- memset(buf, 0x00, sizeof(buf));
- free(namelist[i]);
- }
- len = sprintf(buf, "
"); - send(cfd, buf, len, 0);
- free(namelist);
- return 0;
- }
-
- int hex2dec (char c) {
- if ('0' <= c && c <= '9') return c - '0';
- else if ('a' <= c && c <= 'f') return c - 'a' + 10;
- else if ('A' <= c && c <= 'F') return c - 'A' + 10;
- return 0;
- }
-
- char dec2hex (short int c) {
- if (0 <= c && c <= 9) return c + '0';
- else if (10 <= c && c <= 15) return c + 'A' - 10;
- return 0;
- }
-
- void urlDecode(char* org, char *obj) {
- if (!org || !obj) {
- return;
- }
- //isxdigit:判断字符是否是十六进制字符
- for (; *org != '\0'; ++org, ++obj) {
- if ('%' == org[0] && isxdigit(org[1]) && isxdigit(org[2])) {
- *obj = hex2dec(org[1]) * 16 + hex2dec(org[2]);
- org += 2;
- }
- else {
- *obj = *org;
- }
- }
- }
主程序:
- #include
- #include
- #include "HttpServer.h"
-
- int main(int argc, char** argv) {
- unsigned short int port;
- if (argc < 3) {
- printf("program {port} {path}\n");
- return -1;
- }
- sscanf(argv[1], "%hu", &port);
- printf("begin start http server, listen %d ...\n", port);
- //修改进程的工作目录
- chdir(argv[2]);
- int lfd = initListenFd(port);
- epoolRun(lfd);
-
- return 0;
- }
Makefile 编译脚本:
- app: httpServer tcpServer tcpClient
-
- #说明:$^代表依赖项
- httpServer: CommonUtil.c HttpServer.c main.c
- gcc -g $^ -o httpServer -lpthread
-
- tcpServer: CommonUtil.c TcpServer.c
- gcc -g $^ -o tcpServer
-
- tcpClient: CommonUtil.c TcpClient.c
- gcc -g $^ -o tcpClient
-
- clean:
- -rm httpServer tcpServer tcpClient -f


展示web服务器的根目录:

在线浏览文件:

下载文件:

tcp协议作用途广泛,它是面向连接的流式协议,三次握手建立连接,四次挥手断开连接,其通信流程如下:

头文件:
- #pragma once
- #include
- #include
- #include
- #include
-
- int isLittleEndian();
-
- int readn(int fd, char* buf, int len);
-
- int writen(int fd, char *buf, int len);
源文件
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include "CommonUtil.h"
-
- static const char* _s_showMsg [] = {
- "次北固山下",
- "【作者】王湾 【朝代】唐",
- "客路青山外,行舟绿水前。",
- "潮平两岸阔,风正一帆悬。",
- "海日生残夜,江春入旧年。",
- "乡书何处达?归雁洛阳边。"
- };
-
- static void sendMsg(int fd, const char *data, int len) {
- char *buf = (char *)malloc(sizeof(int) + len);
- int nlen = len;
- if (isLittleEndian()) {
- nlen = htonl(len);
- }
- memcpy(buf, &nlen, sizeof(int));
- memcpy(buf + sizeof(int), data, len);
- printf("发送数据长度:: [%d] 内容:: [%s]\n", len, data);
- writen(fd, buf, sizeof(int) + len);
- if (buf) {
- free(buf);
- }
- }
-
- static int startTcpServer(unsigned short port)
- {
- int lfd = socket(AF_INET, SOCK_STREAM, 0);
- if (-1 == lfd) {
- perror("scoket");
- return -1;
- }
- int opt = -1;
- int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
- if (-1 == ret) {
- perror("setsockopt");
- 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, (const struct sockaddr *)&addr, sizeof(addr));
- if (-1 == ret) {
- perror("bind");
- return -1;
- }
- ret = listen(lfd, 128);
- if (-1 == ret) {
- perror("listen");
- return -1;
- }
- struct sockaddr_in client;
- socklen_t client_addrlen = sizeof(client);
- printf("port %d, wait client accept ...\n", port);
- int connfd = accept(lfd, (struct sockaddr*)(&client), &client_addrlen);
- if (connfd < 0) {
- perror("accept");
- return -1;
- }
- printf("connect client info: addr = %s, port = %d\n",
- inet_ntoa(client.sin_addr), ntohs(client.sin_port));
- int lineNum = sizeof(_s_showMsg) / sizeof(_s_showMsg[0]);
- for (int i = 0; i < lineNum; i++) {
- sendMsg(connfd, _s_showMsg[i], strlen(_s_showMsg[i]));
- usleep(1000 * 100); //此处为了减轻客户端压力
- }
- printf("我活干完了,数据已经全部发送到客户端!\n");
- getchar();
- close(connfd);
- close(lfd);
-
- return 1;
- }
-
- int main(int argc, char**argv) {
- if (argc < 2) {
- printf("a.out {port}\n");
- return -1;
- }
- unsigned short port;
- sscanf(argv[1], "%hu", &port);
- startTcpServer(port);
- return 0;
- }
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include "CommonUtil.h"
-
- static void recvMsg(int fd, char **data, int *len) {
- int nlen = 0;
- int ret = readn(fd, (char*)&nlen, sizeof(int));
- *len = nlen;
- if (isLittleEndian()) {
- *len = ntohl(nlen);
- }
- char *tmp = (char *)malloc(*len + 1);
- readn(fd, tmp, *len);
- tmp[*len] = '\0';
- *data = tmp;
- }
-
- static int startTcpClient(unsigned short port)
- {
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- assert(-1 != sockfd);
- //指定服务器的ip和端口
- struct sockaddr_in saddr;
- memset(&saddr, 0, sizeof(saddr));
- saddr.sin_family = AF_INET;
- saddr.sin_port = htons(port);
- //saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
- inet_pton(sockfd, "127.0.0.1", &saddr.sin_addr.s_addr);
- //作为客户端不需要指定端口,系统自动给客户端设置端口,连接上后直接收发数据
- printf("begin connect port %d\n", port);
- int ret = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
- if (-1 == ret) {
- perror("connect");
- return 0;
- }
- while(1)
- {
- char *buff = NULL;
- int ret = 0;
- recvMsg(sockfd, &buff, &ret);
- if (-1 == ret) {
- perror("read");
- break;
- }
- else if (0 == ret) {
- perror("server quit ");
- break;
- }
- else {
- if (buff) {
- printf("接收数据长度:: [%d] 内容:: [%s]\n", ret, buff);
- free(buff);
- }
- }
- printf("\r\n------------------------------\n\r");
- sleep(rand() % 10);
- }
- close(sockfd);
- exit(0);
- }
-
- int main(int argc, char**argv) {
- if (argc < 2) {
- printf("a.out {port}\n");
- return -1;
- }
- unsigned short port;
- sscanf(argv[1], "%hu", &port);
- startTcpClient(port);
- return 0;
- }
- #include
- #include
- #include
- #include
- #include "CommonUtil.h"
-
- int isLittleEndian() {
- static unsigned short data = 0x1234;
- if (*((char*)&data) == 0x34) {
- return 1;
- }
- else {
- return 0;
- }
- }
-
-
- int readn(int fd, char* buf, int len) {
- int nleft = len;
- int nread = 0;
- char *pbuf = buf;
- while (nleft > 0) {
- nread = read(fd, pbuf, nleft);
- if (-1 == nread) {
- perror("read");
- return -1;
- }
- else if (0 == nread) {
- return len - nleft;
- }
- pbuf += nread;
- nleft -= nread;
- }
- return len;
- }
-
- int writen(int fd, char *buf, int len) {
- int nleft = len;
- int nwrite = 0;
- char *pbuf = buf;
- while (nleft > 0) {
- nwrite = write(fd, pbuf, nleft);
- if (-1 == nwrite) {
- perror("write");
- return -1;
- }
- else if (0 == nwrite) {
- continue;
- }
- pbuf += nwrite;
- nleft -= nwrite;
- }
- return len;
- }
编译脚本:
- app: httpServer tcpServer tcpClient
-
- #说明:$^代表依赖项
- httpServer: CommonUtil.c HttpServer.c main.c
- gcc -g $^ -o httpServer -lpthread
-
- tcpServer: CommonUtil.c TcpServer.c
- gcc -g $^ -o tcpServer
-
- tcpClient: CommonUtil.c TcpClient.c
- gcc -g $^ -o tcpClient
-
- clean:
- -rm httpServer tcpServer tcpClient -f

源码下载路径如下:
https://download.csdn.net/download/hsy12342611/87183336
只有平时多接触底层编程才能体会到一些技术的本质,做技术不能被表面的虚幻所迷惑,要从本质上去理解一些东西,好的,今天就到这里了,该去休息了。