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协议

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容