在实际开发中经常用到web框架,比如Servlet,SpringBoot等,这些开发框架提高了我们的开发效率,节省了开发时间。但是这会令我们技术人员处于浮云之上,看不到其本质。说实话,Java语言那么流行,其本质是运行在JRE虚拟机上的,而JRE是用C/C++语言开发的。与其说java跨平台,不如说是因为在不同平台上各自实现JRE,从而屏蔽了java语言直接与不同平台打交道。http协议广泛应用,也是基于TCP协议之上的封装。本节博主将带领大家用C语言在Linux环境下开发HTTP服务器,支持浏览器下载和浏览文件。另外还使用TCP协议开发了服务端和客户端来实现服务端监听客户端连接,然后向其发送一首唐诗。
目录
1. 关键函数说明
1.1 IP地址字节序转换
1.2 端口字节序转换
1.3 socket
1.4 bind
1.5 listen
1.6 accept
1.7 接收数据
1.8 发送数据
1.9 connect
2. HTTP服务器
2.1 源码
2.2 效果
3.TCP服务器和客户端
3.1 源码
3.2 效果
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
在线浏览文件:
下载文件:
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
只有平时多接触底层编程才能体会到一些技术的本质,做技术不能被表面的虚幻所迷惑,要从本质上去理解一些东西,好的,今天就到这里了,该去休息了。