C语言实现简单Web服务器(一)

DWBServer

我们这次要完成的最终结果如上图所示

前置知识

  • C语言
  • Linux Socket编程
  • 基本的网络知识
  • Unix/Linux 基本知识
Socket通信模式

一图胜千言,可以看出Socket编程主要分为这7个步骤,这次我们主要编写服务器端的代码,客户端由浏览器代理。

Socket在OSI七层模型中的位置

网络层的IP协议使用IP地址唯一的标识了一台主机,而传输层的协议使用协议名+端口号唯一的标识了系统的一个进程,所以我们才可以利用socket在不同主机的进程间通信

创建一个socket

int socket(int domain, int type, int protocol);

这是创建socket的函数原型

domain
中文意思为域,可传的值为AF_UNIXAF_LOCALAF_INETAF意为Adress Family。前两个为本机操作,最后一个为IPv4的网络操作,所以为AF_INET

type
类型,可传值为SOCK_STREAMSOCK_DGRAMSOCK_PACKET
SOCK_STREAM 使用 TCP 协议传输数据,SOCK_DGRAM 使用 UDP 协议传输数据,我们要做的是Web服务器,肯定是选择面向连接的可靠的TCP协议,所以这个值传SOCK_STREAM

protocol:
所用的协议,有IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTP,传0为自动选择协议,所以我们传0

返回值
返回一个socket描述符(socket descriptor),它唯一标识一个socket,这个socket描述字跟文件描述字一样。

int server_socket = socket(AF_INET, SOCK_STREAM, 0);

将socket和地址绑定

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:
socket文件描述符,socket()函数的返回值,也就是server_socket

addr:
指向地址结构体的指针,这是一个struct sockaddr类型的通用指针,我们实际创建的结构体为

struct sockaddr_in {
    __uint8_t   sin_len;
    sa_family_t sin_family;
    in_port_t   sin_port;
    struct  in_addr sin_addr;
    char        sin_zero[8];
};

传递的时候需要做强制类型转换

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);

注意这里的网络字节序和主机字节序的转换,INADDR_ANY表示任何网络地址都可以访问

memset函数初始化server_addr各个字节为0,防止有未初始化的垃圾值存在

addrlen:
结构体的长度,由于在函数内部无法获取到结构体长度(因为传递的是指针,参考数组),所以需要把长度传入

返回值:
绑定成功或者失败的消息码,暂时不作处理

bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));

监听

int listen(int sockfd, int backlog);

sockfd:
socket文件描述符,socket()函数的返回值,也就是server_socket

backlog:
socket待连接队列的最大个数,一般为5

返回值:
绑定成功或者失败的消息码,暂时不作处理

listen(server_socket, 5);

与客户端建立连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:
socket文件描述符,socket()函数的返回值,也就是server_socket

addr:
客户端地址信息的结构体,不关心可以传NULL

addrlen:
客户端地址长度,不关心可以传NULL

返回值:
socket文件描述符,在与客户端建立连接后,accpet还是会生成一个专门用于和当前客户端通信的socket,而原来那个socket照常负责和其他等待建立连接的客户端建立通信

int client_socket = accept(server_socket, NULL, NULL);

从浏览器读取请求内容

ssize_t read(int fd, void *buf, size_t count);

fd:
文件描述符,从哪个文件读
buf
读的内容存到buf中
count:
共读多少个字节

char buf[1024];
read(client_socket, buf, 1024);

记住,在Linux,一切皆文件,网络接口、甚至鼠标键盘显示器都是文件

往浏览器写响应内容

ssize_t write(int fd, const void *buf, size_t count);

fd:
文件描述符,往哪个文件写
buf
内容的首地址
count:
共读多少个字节

char status[] = "HTTP/1.0 200 OK\r\n";
char header[] = "Server: DWBServer\r\nContent-Type: text/html;charset=utf-8\r\n\r\n";
char body[] = "<html><head><title>C语言构建小型Web服务器</title></head><body><h2>欢迎</h2><p>Hello,World</p></body></html>";

write(client_socket, status, sizeof(status));
write(client_socket, header, sizeof(header));
write(client_socket, body, sizeof(body));

写的格式是按HTTP协议响应报文的格式写的,响应报文的格式为响应行+响应首部+响应体,注意响应首部响应体之间有一个空行

在浏览器中输入http://localhost:8080/, 就会出现

DWBServer

用Charles抓包

请求
响应

关闭连接

int close(int fd);
close(client_socket);
close(server_socket);

最后把两个socket全部关闭

完整代码

#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

#define PORT 8080                       // 服务器监听端口

int main(){
    
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);
    
    bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
    
    listen(server_socket, 5);
    
    int client_socket = accept(server_socket, NULL, NULL);
    
    char buf[1024];
    read(client_socket, buf, 1024);
    
    printf("%s",buf);

    char status[] = "HTTP/1.0 200 OK\r\n";
    char header[] = "Server: DWBServer\r\nContent-Type: text/html;charset=utf-8\r\n\r\n";
    char body[] = "<html><head><title>C语言构建小型Web服务器</title></head><body><h2>欢迎</h2><p>Hello,World</p></body></html>";

    write(client_socket, status, sizeof(status));
    write(client_socket, header, sizeof(header));
    write(client_socket, body, sizeof(body));

    close(client_socket);
    close(server_socket);

    return 0;
}

结语

至此,我们已经用socket实现了一个最简单的Web服务器(其实还算不上,只是一个浏览器充当clientsocket小程序),下一篇继续完善这个Web服务器,加入处理Get请求的逻辑,进一步实现HTTP协议

推荐阅读更多精彩内容