开启掘金生长之旅!这是我参加「掘金日新计划 12 月更文挑战」的第6天,点击查看活动概况

完成: 运用C语言完成最简略的HTTP服务器 (源代码附在文章底部)

要求:

  1. 一起支撑HTTP(80端口)和HTTPS(443端口)运用两个线程别离监听各自端口
  2. 只需支撑GET办法,解析恳求报文,回来相应应对及内容
  3. 支撑的状况码

用C语言写一个支持HTML和视频的Http(s)服务器

试验流程:

  1. 依据上述要求,完成HTTP服务器程序
  2. 执行sudo python topo.py指令,生成包括两个端节点的网络拓扑
  3. 在主机h1上运转HTTP服务器程序,一起监听80和443端口 h1 # ./http-server
  4. 在主机h2上运转测验程序,验证程序正确性 h2 # python3 test/test.py 假如没有呈现AssertionError或其他过错,则阐明程序完成正确
  5. 在主机h1上运转http-server,所在目录下有一个小视频(30秒左右)
  6. 在主机h2上运转vlc(留意切换成普通用户),经过网络获取并播映该小视频 媒体 -> 翻开网络串流 -> 网络 -> 请输入网络URL -> 播映
socket编程基础

名词解析:socket有“插座”的意思,在linux环境下socket是进程间网络通讯的特殊文件类型。编程中一般运用文件描述符fd来引用套接字,fd是一个int类型的文件描述符。简略解说下socket怎么使得两个进程彼此通讯。 socket端:

  1. 运用sockaddr_in结构创立服务器地址server_addr,对地址进行初始化
  2. 设置地址的协议族,IP号、端口号
  3. 创立一个socket,并回来到sfd,sfd此时能引用这个server sockt
  4. 将server_addr和服务端文件描述符(sfd)绑定起来
  5. 运用listen函数对sfd进行监听,也便是对指定的IP和端口号进行监听
  6. 堵塞等候接纳客户端的恳求(客户端只要恳求正确的IP和端口才干正常衔接服务器,所以客户端会有一个和服务器端相同的server_addr)
  7. 比及client拿自己的cfd(客户端文件描述符)去衔接server,server会新建一个socket ,这个socket指向connfd, server可以read这个connfd的数据,然后再将信息write到这个connfd上,connfd和cfd实际上指向的是同一个socket,客户端可以从这个cfd上读取server写进去的数据。这样就完成了c/s的通讯。
  8. 封闭socket衔接

client端:

  1. 创立socket,回来到cfd
  2. 运用sockaddr_in结构创立服务器地址server_addr,对地址进行初始化。并将成员值设置为和上面server_addr相同
  3. 运用cfd和server_addr作为参数去衔接指定的server端口。
  4. 衔接成功后,client能向cfd指向的socket写数据,server树立衔接后会创立新的socket,这个socket实际和cfd指向的是socket是一致的。client可以向这个socket写入数据和读取数据。
  5. 封闭socket衔接

经过socket完成能传输简略信息的http服务器

server端

int main() {
  struct sockaddr_in server_addr, client_addr;
  int listenfd, connfd;  //监听套接字和衔接套接字
  char buffer[MAXLINE], first_line[MAXLINE], left_line[MAXLINE], method[MAXLINE], url[MAXLINE], version[MAXLINE];
  char str[INET_ADDRSTRLEN];
  socklen_t client_addr_len;
  char filename[MAXLINE];
  long n;
  int i, pid;
  listenfd = socket(AF_INET, SOCK_STREAM, 0);   //创立套接字
  bzero(&server_addr, sizeof(server_addr));    //初始化server_addr
  server_addr.sin_family = AF_INET;        //设置协议族(IPV4)
  server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//具体IP地址
  server_addr.sin_port = htons(PORT1);       //设置端口号
  printf("threadID: %d, server_addr: %s, port: %d", pthread_self(), inet_ntop(AF_INET, &server_addr.sin_addr, str, sizeof(str)), ntohs(server_addr.sin_port));
  int bind_status = bind(listenfd, (struct sockaddr*)&server_addr, sizeof(server_addr));  //绑定套接字
  if(bind_status < 0)  printf("bind error!\n");
  int listen_status = listen(listenfd, 20);  //监听套接字,等候用户发起恳求
  if(listen_status < 0) printf("listen error!\n");
  printf("Accepting connections ...");
  while(1) {
    //堵塞等候承受客户端的恳求
    client_addr_len = sizeof(client_addr);
    connfd = accept(listenfd, (struct sockaddr*)&client_addr, &client_addr_len);  //承受客户端的恳求
    n = read(connfd, buffer, MAXLINE);
    if (n == 0) {
      printf("client has been closed");
      break;
    }
    printf("port %d Received from %s at PORT %d, message is %s\n",
        ntohs(server_addr.sin_port),
        inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
        ntohs(client_addr.sin_port), buffer);
    //解析恳求报文
    for (int i = 0; i < n; i++) {
      buffer[i] = toupper(buffer[i]);
    }
    write(connfd, buffer, n);
    close(connfd);
  }
}

为了便利测验,在topo.py中将两个host改为3个host,并设置衔接。修正如下 (topo.py用来设置网络参数并发动mininet,参见网络广播试验/post/717200…

class MyTopo(Topo):
  def build(self):
    h1 = self.addHost('h1')
    h2 = self.addHost('h2')
    h3 = self.addHost('h3')
    s1 = self.addSwitch('s1')
    self.addLink(h1, s1, bw=100, delay='10ms')
    self.addLink(h2, s1)
    self.addLink(h3, s1)

在h1中运转./server,在h2和h3中别离运转./client1和./client2。client1和client2的区别是恳求的端口不同,但主机IP是相同的,可以用来测验server是否能一起检测两个端口。结果展现如下

用C语言写一个支持HTML和视频的Http(s)服务器

client1.c如下所示


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//read办法需求的头文件
#include <unistd.h>
//socket办法需求的头文件
#include <sys/socket.h>
#include <sys/types.h>
//htonl 办法需求的头文件
#include <netinet/in.h>
//inet_ntop办法需求的头文件
#include <arpa/inet.h>
#define MAXLINE 100
#define CLI_PORT 80
//webserver 主程序
int main(int argc, const char * argv[]) {
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int clientfd;
    long n;
    //client socket衔接
    clientfd = socket(AF_INET, SOCK_STREAM, 0);
    char *str = "hello world";
    //sockaddr_in结构体初始化
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(CLI_PORT);
    //connect()办法
    connect(clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    //write()办法是client 向 server 写数据
    write(clientfd, str, strlen(str));
    //最好用strlen,否则server接纳的数据会少
    printf("write to server : %s\n",str);
    //read()办法是从server接纳数据
    n = read(clientfd, buf, sizeof(buf));   
    //留意最好用sizeof而不是strlen。试验证明用strlen时承受会犯错。bug调试好久才找到
    printf("%ld\n",n);
    if(n == 0) {
        printf("the other side has been close\n");
    }else {
        printf("Response from server: %s\n",buf);
        write(STDOUT_FILENO, buf, n);
        printf("\n");
    }
    close(clientfd);
}

现在客户端能和服务器进行简略的通讯,下一步便是要支撑略微复杂些的通讯。

支撑并发的http服务器
  1. 首先将socket函数封装为带有容错机制的函数,运转进程中犯错的话可以更好的定位过错。
int Socket(int family, int type, int protocol) {
    int sockfd;
    if((sockfd = socket(family, type, protocol)) < 0)
    {
        perror("socket error");
        exit(1);
    }
    return sockfd;
}
void Bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    if(bind(sockfd, addr, addrlen) < 0)
    {
        perror("bind error");
        exit(1);
    }
}
void Listen(int sockfd, int backlog) {
    if(listen(sockfd, backlog) < 0)
    {
        perror("listen error");
        exit(1);
    }
}
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
    int connfd;
    if((connfd = accept(sockfd, addr, addrlen)) < 0)
    {
        perror("accept error");
        exit(1);
    }
    return connfd;
}
void Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    if(connect(sockfd, addr, addrlen) < 0)
    {
        perror("connect error");
        exit(1);
    }
}
long Read(int fd, void *buf, size_t count) {
    long n;
    if((n = read(fd, buf, count)) < 0)
    {
        perror("read error");
        exit(1);
    }
    return n;
}
void Write(int fd, void *buf, size_t count) {
    if(write(fd, buf, count) < 0)
    {
        perror("write error");
        exit(1);
    }
}
void Close(int fd) {
    if(close(fd) < 0)
    {
        perror("close error");
        exit(1);
    }
}
  1. 使服务器带有并发才干,也便是http server接纳到一个恳求就会fork()一个进程去处理恳求。 下面是部分逻辑代码: 当server Accept一个恳求时,fork一个进程,假如是子进程,则对该恳求处理。假如没有正常fork,pid仍是本来大于0的父进程,则直接封闭socket。
    //死循环中进行accept()
    while (1) {
        cliaddr_len = sizeof(cliaddr);
        //accept()函数回来一个connfd描述符
        connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, 
						&cliaddr_len);
        pid = fork();
        if(pid < 0) {
            printf("fork error");
            return 1;
        } else if(pid == 0) {   //pid=0表示子进程
            while (1) {
                n = Read(connfd, buf, MAXLINE);
                if (n == 0) {
                    printf("the other side has been closed.\n");
                    break;
                }
                printf("received from %s at PORT %d,message is %s,\ 
						 message size is %ld\n",
                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, 
							  sizeof(str)),
			                  ntohs(cliaddr.sin_port),buf, n);
                for (int i = 0; i < n; i++)
                    buf[i] = toupper(buf[i]);
                Write(connfd, buf, n);
            }
                Close(connfd);
                exit(0);
        }else {  //pid>0表示父进程
            Close(connfd);
        }     
    }

从下图可以看出,运用了多进程并发机制后,当有多个恳求时,会fork子进程,服务器端的进程对应也在添加。

用C语言写一个支持HTML和视频的Http(s)服务器

支撑html页面的http server
  1. 制作html页面,试验初始代码中包括国科大官网的页面,部分源码如下:

用C语言写一个支持HTML和视频的Http(s)服务器

  1. 翻开服务器,运用firefox输入 http://localhost:80/index.html ,查看web发到服务器中的内容。 整个头部信息如下所示,在代码中被存在buf中。**留意我们在read() socket时,是要将内容放到buf中,可以设置buf的大小,假如buf设置太小,则需求循环read,直到connfd中的内容被读空

用C语言写一个支持HTML和视频的Http(s)服务器

运用下面代码来解析恳求

sscanf(buf, "%s %s %s", method, uri, version);  //对buf进行解析
printf("method:%s\n", method);
printf("uri:%s\n", uri);
printf("version:%s\n", version);
  1. 依据恳求中的url来查找服务器本地文件。假如在文件夹下找到文件名,则获取文件类型,拼接response回来给服务器终端。别的需求读取文件信息并将其输入给浏览器上,首要的函数是mmap(0, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

呼应函数response如下所示:

void response(int connfd, char *filename) {
    struct stat sbuf;   //文件状况结构体
    int fd;
    char *srcp;
    char response[MAXLINE], filetype[20];
    if (stat(filename, &sbuf) < 0) {
        //文件不存在
        sprintf(response, "HTTP/1.1 404 Not Found\r");
        exit(1);
    }
    else {
        get_filetype(filename, filetype);   //获取文件类型
        //Open File
        fd = open(filename, O_RDONLY);
        //Send response 这是是在进行拼接,一定要回来头部,
        //客户端会先识别头部然后对数据部分进行个性化解析
        strcat(response, "HTTP/1.1 200 OK\r\n");
        strcat(response, "Server: LongXing's Tiny Web Server\r\n");
        sprintf(response, "Content-length: %ld\r\n", sbuf.st_size);
        sprintf(response, "Content-type: %s\r\n\r\n", filetype);
        Write(connfd, response, strlen(response));
        printf("Response headers:\n");
        printf("%s", response);
        //mmap()读取filename 的内容写给浏览器 
        srcp = mmap(0, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
        //内存映射,直接将fd指向的文件映射到srcp上,而不先将磁盘上的文件读取到内核缓冲区, 
        //再从内核缓冲区将文件进程虚拟地址空间。它映射完了就可以直接运用,只要一次读取。
        Close(fd);
        Write(connfd, srcp, sbuf.st_size);
        munmap(srcp, sbuf.st_size);
    }
}

现在可以在本地浏览器中输入恳求并得到呼应,由于html中的一些图标文件在服务器本地是缺失的,所以有些文件不会显示。

HTTP恳求和呼应展现

linux虚拟机中HTTP恳求index.html

用C语言写一个支持HTML和视频的Http(s)服务器

虚拟机外部的主机浏览器HTTP恳求index.html

用C语言写一个支持HTML和视频的Http(s)服务器

用C语言写一个支持HTML和视频的Http(s)服务器

支撑HTTPS的服务器

HTTPS基础
现在大部分网站都支撑https,这是一种安全的http协议。不同于HTTP的明文传输,HTTPS运用SSL/TSL来加密数据包,一般是运用协商的对称加密算法和密钥加密,确保数据机密性。假如不运用HTTPS,当攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其间的信息,这对大多用户来说都是不肯面对的。
运用 HTTPS 协议需求到 CA(Certificate Authority,数字证书认证机构) 申请证书,然后在网页服务器代码中进行SSL编程,假如不运用A证书,只运用自己的用户证书(.cert)和用户私钥(.prokey),安全证书不会经过,导致浏览器衔接时需额定确认是否需求进行不安全衔接,在curl时需求加上--insecure参数才干进行恳求。
HTTPS树立进程和HTTP不同,后者只需求三次握手就能树立,而HTTPS需求额定九次SSL握手,所以一共是12个包。因此,HTTP的呼应速度是要比HTTP快。别的,HTTP运用80端口,而HTTPS运用443端口。
HTTPS的恳求树立进程

用C语言写一个支持HTML和视频的Http(s)服务器

HTTPS中SSL部分代码

//封装的部分SSL代码
long SSL_Read(SSL *ssl, void *buf, size_t count) {
    long n;
    if((n = SSL_read(ssl, buf, count)) < 0)
    {
        perror("read error");
        exit(1);
    }
    return n;
}
void SSL_Write(SSL *ssl, void *buf, size_t count) {
    if(SSL_write(ssl, buf, count) < 0)
    {
        perror("write error");
        exit(1);
    }
}
//SSL编程流程
//再收到客户端来的https恳求,先accept回来connfd,之后再
//1. 初始化SSL
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
//2.加载用户证书和私钥以及对其进行查看
SSL_CTX *ctx = SSL_CTX_new(SSLv23_server_method()); //创立服务端SSL会话环境
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
if(SSL_CTX_use_certificate_file(ctx, "cnlab.cert", SSL_FILETYPE_PEM) <= 0) {
	//加载公钥证书
	printf("load public key error");
	exit(1);
}
printf("加载私钥...\n");
if(SSL_CTX_use_PrivateKey_file(ctx, "cnlab.prikey", SSL_FILETYPE_PEM) <= 0) {
	//加载私钥
	printf("load private key error");
	exit(1);
}
printf("验证私钥...\n");
if(SSL_CTX_check_private_key(ctx) <= 0) {
	//查看私钥
	printf("check private key error");
	exit(1);
}
//3. 依据ctx创立一个ssl
SSL *ssl = SSL_new(ctx);
//4. 将fd与ssl绑定
SSL_set_fd(ssl, fd)
//5.再次进行Accept,运用SSL_accept(ssl)获取客户端的恳求
if(SSL_accept(ssl) == -1) {
    ERR_print_errors_fp(stderr);
}
//进行呼应,运用SSL_Read进行读,运用SSL_Write进行写操作

下面是针对HTTPS的呼应函数

void https_response(SSL *ssl, int connfd, char *filename) {
    struct stat sbuf;   //文件状况结构体
    int fd;
    char *srcp;
    char response[MAXLINE], filetype[20];
    if (stat(filename, &sbuf) < 0) {
        //文件不存在
        sprintf(response, "HTTP/1.1 404 Not Found\r");
        printf("找不到文件\n");
        exit(1);
    }
    else {
        get_filetype(filename, filetype);   //获取文件类型
        //Open File
        fd = open(filename, O_RDONLY);
        //Send response 这是是在进行拼接
        strcat(response, "HTTP/1.0 200 OK\r\n");
        SSL_Write(ssl, response, strlen(response));
        strcat(response, "Server: LongXing's Tiny Web Server\r\n");
        SSL_Write(ssl, response, strlen(response));
        sprintf(response, "Content-length: %ld\r\n", sbuf.st_size);
        SSL_Write(ssl, response, strlen(response));
        sprintf(response, "Content-type: %s\r\n\r\n", filetype);
        SSL_Write(ssl, response, strlen(response));
        printf("Response headers:\n");
        printf("%s", response);
        //mmap()读取filename 的内容写给浏览器 
        srcp = mmap(0, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
        SSL_Write(ssl, srcp, sbuf.st_size);
        munmap(srcp, sbuf.st_size);
        Close(fd);
    }
}
HTTPS恳求和呼应展现

HTTPS在443端口恳求index.html,留意有不安全提示是因为运用了自签证书而不是官方证书

用C语言写一个支持HTML和视频的Http(s)服务器

HTTPS在443端口恳求video.mp4

用C语言写一个支持HTML和视频的Http(s)服务器

运用telnet模仿浏览器对index.html恳求

用C语言写一个支持HTML和视频的Http(s)服务器

一起支撑HTTP和HTTPS的服务器

思路:将支撑HTTPS的代码从新建socket、绑定、监听、Accept、呼应等操作全部封装到https_server函数中。将支撑HTTP恳求的所有代码封装到http_server函数中。在主函数main中,树立两个子进程,子进程1运转http_server函数,子进程2运转https_server函数。这样在同一个服务器中能有两个socket,一个监听支撑HTTP恳求的80端口,别的一个监听支撑HTTPS恳求的443端口。mian函数如下:

int main(int argc, char * argv[]) {
    pid_t pid1, pid2;
    pid1 = fork();
    if(pid1 < 0) printf("fork1 error\n");
    if(pid1 == 0)   http_server();
    pid2 = fork();
    if(pid2 < 0) printf("fork2 error\n");
    if(pid2 == 0)   https_server();
    int st1, st2;
    waitpid(pid1, &st1, 0);
    waitpid(pid2, &st2, 0);
    return 0;
}

经过多进程,当客户端进行HTTP恳求,进程1会监听到并进行呼应,当客户端进行HTTPS恳求,进程2会监听到并进行呼应。完成了一起支撑HTTP和HTTPS的服务器,而且可以传输正常HTML页面以及视频流。

遇到的问题
  1. 不了解socket编程
  2. 不了解https的作业机制
  3. 不了解response中报文头部的构造及重要性 在查阅材料和不断试验后,对上述问题有了开始的了解并加以解决了。
总结

构造一个简略的http服务器,尽管试验项目不大,但却使自己对计算机网络教材中的概念更加明晰。实践是检验真理的唯一标准,只要经过实际操作才干检验自己对讲义上的知识是否真的了了解。别的,这次的试验也锻炼自己发现和解决问题的才干。总而言之,获益匪浅。

Github源码地址: github.com/LongxingHu/…