Linux下C语言实现HTTP文件服务器和TCP协议实现网络数据传输
Linux C语言http协议开发web文件浏览服务器tcp协议开发服务器和客户端来进行数据传输
在实际开发中经常用到web框架,比如Servlet,SpringBoot等,这些开发框架提高了我们的开发效率,节省了开发时间。但是这会令我们技术人员处于浮云之上,看不到其本质。说实话,Java语言那么流行,其本质是运行在JRE虚拟机上的,而JRE是用C/C++语言开发的。与其说java跨平台,不如说是因为在不同平台上各自实现JRE,从而屏蔽了java语言直接与不同平台打交道。http协议广泛应用,也是基于TCP协议之上的封装。本节博主将带领大家用C语言在Linux环境下开发HTTP服务器,支持浏览器下载和浏览文件。另外还使用TCP协议开发了服务端和客户端来实现服务端监听客户端连接,然后向其发送一首唐诗。
目录
1. 关键函数说明
1.1 IP地址字节序转换
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.2 端口字节序转换
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);
1.3 socket
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
1.4 bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:将文件描述符和本地的IP与端口进行绑定
参数:
sockfd: 监听的文件描述符,通过 socket () 调用得到
addr: 要绑定的 IP 和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序
addrlen: 参数 addr 指向的内存大小
返回值:成功返回 0,失败返回 - 1
1.5 listen
作用:监听套接字
int listen(int sockfd, int backlog);
参数:
sockfd: 文件描述符,调用 socket () 得到
backlog: 同时能处理的最大连接,最大值为 128
返回值:成功返回 0,失败返回 -1
1.6 accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
作用:等待并接受客户端的连接请求, 建立新的连接, 得到一个通信的文件描述符,函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞。
当检测到有新的客户端连接时,解除阻塞,得到的描述符就可以和客户端通信。
参数:
sockfd: 监听的文件描述符
addr: 建立连接的客户端的地址信息
addrlen: addr 指向的内存大小
返回值:函数调用成功,得到一个文件描述符,调用失败返回 -1
1.7 接收数据
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。
1.8 发送数据
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:发送数据失败了
1.9 connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:与服务器建立连接
参数:
sockfd: socket 返回值
addr: 要连接的服务器的 iP 和 端口,这个 IP 和端口都是大端的
addrlen: addr内存大小
返回值:连接成功返回 0,连接失败返回 - 1
2. HTTP服务器
http服务器实现建立在tcp协议之上,采用epoll和多线程的方式接收和处理客户端。解析从TCP传过来的数据,解析请求行,找到需要访问的资源。如果是目录则使用html的a标签href属性进行重定位,进入目录。如果是文件,txt和png文件浏览器在线预览,其他文件则让浏览器进行下载。
2.1 源码
头文件:
#pragma once
#include <pthread.h>
//线程参数结构
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 <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <ctype.h>
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, "<html><head><title>%s</title><body><table>", 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, "<tr><td><a href=\"%s/\" style=\"font-size:20px\">%s</a> </td><td>%ld</td></tr>", \
name, name, st.st_size);
}
else {
ret = sprintf(buf + len, "<tr><td><a href=\"%s\" style=\"font-size:20px\">%s</a> </td><td>%ld</td></tr>",
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, "</table></head></body></html>");
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 <stdio.h>
#include <unistd.h>
#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
2.2 效果
展示web服务器的根目录:
在线浏览文件:
下载文件:
3.TCP服务器和客户端
tcp协议作用途广泛,它是面向连接的流式协议,三次握手建立连接,四次挥手断开连接,其通信流程如下:
3.1 源码
头文件:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int isLittleEndian();
int readn(int fd, char* buf, int len);
int writen(int fd, char *buf, int len);
源文件
#include <strings.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#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 <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#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
3.2 效果
源码下载路径如下:
https://download.csdn.net/download/hsy12342611/87183336
只有平时多接触底层编程才能体会到一些技术的本质,做技术不能被表面的虚幻所迷惑,要从本质上去理解一些东西,好的,今天就到这里了,该去休息了。
更多推荐
所有评论(0)