我们这次要完成的最终结果如上图所示
前置知识
- C语言
-
Linux Socket
编程 - 基本的网络知识
-
Unix/Linux
基本知识
一图胜千言,可以看出Socket
编程主要分为这7个步骤,这次我们主要编写服务器端的代码,客户端由浏览器代理。
网络层的
IP协议
使用
IP地址
唯一的标识了一台主机,而传输层的协议使用
协议名+端口号
唯一的标识了系统的一个进程,所以我们才可以利用
socket
在不同主机的进程间通信
创建一个socket
int socket(int domain, int type, int protocol);
这是创建socket的函数原型
domain:
中文意思为域,可传的值为AF_UNIX
、AF_LOCAL
、AF_INET
,AF
意为Adress Family
。前两个为本机操作,最后一个为IPv4
的网络操作,所以为AF_INET
type:
类型,可传值为SOCK_STREAM
、SOCK_DGRAM
、SOCK_PACKET
等
SOCK_STREAM
使用 TCP 协议
传输数据,SOCK_DGRAM
使用 UDP 协议
传输数据,我们要做的是Web服务器
,肯定是选择面向连接的可靠的TCP协议
,所以这个值传SOCK_STREAM
protocol:
所用的协议,有IPPROTO_TCP
、IPPTOTO_UDP
、IPPROTO_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[] = "C语言构建小型Web服务器 欢迎
Hello,World
";
write(client_socket, status, sizeof(status));
write(client_socket, header, sizeof(header));
write(client_socket, body, sizeof(body));
写的格式是按HTTP
协议响应报文的格式写的,响应报文的格式为响应行+响应首部+响应体
,注意响应首部
和响应体
之间有一个空行
在浏览器中输入http://localhost:8080/, 就会出现
用Charles抓包
关闭连接
int close(int fd);
close(client_socket);
close(server_socket);
最后把两个socket
全部关闭
完整代码
#include
#include
#include
#include
#include
#include
#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[] = "C语言构建小型Web服务器 欢迎
Hello,World
";
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服务器
(其实还算不上,只是一个浏览器充当client
的socket
小程序),下一篇继续完善这个Web服务器
,加入处理Get
请求的逻辑,进一步实现HTTP
协议