0、前言
准备做一个Linux网络服务器应用实战,通过网页和运行在Linux下的服务器程序通信,这是第五篇,编写服务器程序,与编写好的登录界面进行初步调试。
1、服务器编程
1.1 TCP服务器编程
在之前的登录界面中,我们指定了登录服务器的IP和端口号,其中IP即为服务器的IP地址,端口号即为服务器监听的端口,服务器编程即为TCP编程,具体流程可参考之前的文章Linux应用 TCP网络编程,HTTP每次与服务器通信都会创建一个TCP连接,初步编写代码如下,接收客户端发送的数据进行打印:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#define PORT 8081
#define MAX_SIZE 1024 * 10
int main()
{
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建TCP套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定套接字到指定端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听端口
if (listen(server_fd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d\n", PORT);
while(1)
{
printf("waiting......\n");
// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0)
{
perror("accept failed");
exit(EXIT_FAILURE);
}
char rcvbuffer[MAX_SIZE] = {0};
bzero((char *)rcvbuffer, sizeof(rcvbuffer));
int bytesReceived = read(new_socket, rcvbuffer, MAX_SIZE);
printf("bytesReceived = %d\n", bytesReceived);
if (bytesReceived > 0)
{
printf("Received request: \n%s\n", rcvbuffer);
}
close(new_socket);
}
close(server_fd);
return 0;
}
在虚拟机上运行该服务程序,注意网页中的IP地址和端口号要与虚拟机地址还有服务器监听的端口号一致,测试结果如下:
1.2 OPTIONS处理
在登录界面连接的过程中服务器并没有按期望收到POST请求,而是收到了OPTIONS,查阅了相关资料:OPTIONS请求在CORS(跨源资源共享)机制中是一个预检请求(preflight request)。当浏览器遇到一个非简单请求(non-simple request)时,它会在实际请求之前自动发送一个OPTIONS请求到服务器,以检查服务器是否允许这个跨域请求。
因此,服务器首先需要处理OPTIONS请求然后才能收到正常的POST,简单添加一下处理代码:
if (bytesReceived > 0)
{
printf("Received request: \n%s\n", rcvbuffer);
if(strstr(rcvbuffer, "OPTIONS") != NULL)
{
// 构造CORS响应头部
const char* headers = "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type\r\n"
"Access-Control-Max-Age: 86400\r\n"
"X-Content-Type-Options: nosniff\r\n"
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
"Content-Length: 0\r\n"
"Connection: close\r\n"
"\r\n";
printf("send:\n%s", headers);
send(new_socket, headers, strlen(headers), 0);
}
}
继续测试,可以收到携带账号密码的POST请求:
1.3 登录请求处理
收到登录请求后可以获取到POST中携带的用户名和密码,服务器验证通过后回复成功,登录侧就可以显示成功,先简单添加一下登录处理,检测到登录后直接回复成功,修改代码如下:
if (bytesReceived > 0)
{
printf("Received request: \n%s\n", rcvbuffer);
if(strstr(rcvbuffer, "OPTIONS") != NULL)
{
// 构造CORS响应头部
const char* headers = "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type\r\n"
"Access-Control-Max-Age: 86400\r\n"
"X-Content-Type-Options: nosniff\r\n"
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
"Content-Length: 0\r\n"
"Connection: close\r\n"
"\r\n";
printf("send:\n%s", headers);
send(new_socket, headers, strlen(headers), 0);
}
else if(strstr(rcvbuffer, "username") != NULL)
{
const char* json_response = "{\"status\":\"ok\"}";
int json_response_length = strlen(json_response);
// 构造HTTP响应头部
const char* http_version = "HTTP/1.1";
const char* status_code = "200";
const char* status_message = "OK";
const char* access_control_allow_origin = "Access-Control-Allow-Origin: *\r\n";
const char* content_type = "Content-Type: application/json\r\n";
const char* content_length = "Content-Length: ";
char content_length_header[32];
snprintf(content_length_header, sizeof(content_length_header), "%d", json_response_length);
// 构造完整的HTTP响应
char response[1024] = {0}; // 假设响应不会超过1024字节
snprintf(response, sizeof(response),
"%s %s %s\r\n"
"%s"
"%s"
"%s%s\r\n"
"\r\n"
"%s",
http_version, status_code, status_message, access_control_allow_origin,
content_type, content_length_header, "\r\n",
json_response);
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
}
再进行登录测试发现可以显示登录成功:
1.4 查询请求
修改查询界面HTML代码,添加按键点击后向服务器发送不同的请求命令:
<script type="text/javascript">
function queryData(query) {
var url = 'http://172.21.247.112:8081/query?q=' + query;
fetch(url)
.then(response => response.text())
.then(data => {
document.getElementById("result").value = data;
})
.catch(error => {
console.error('请求出错:', error);
alert("error!");
});
}
</script>
在服务器端添加简单的匹配字符串处理:
else if(strstr(rcvbuffer, "query1") != NULL)
{
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nHello from server Reply to query1";
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
else if(strstr(rcvbuffer, "query2") != NULL)
{
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nHello from server Reply to query2";
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
else if(strstr(rcvbuffer, "query3") != NULL)
{
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nHello from server Reply to query3";
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
else if(strstr(rcvbuffer, "query4") != NULL)
{
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nHello from server Reply to query4";
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
最终实现如下效果,点击不同的按键收到服务器不同的回复:
2、优化
我们在服务器测编写文件发送接口,将HTML文件发送至客户端,客户端只需要输入服务器的地址端口等信息就可以从服务器获取登录界面,登录界面登录成功后向服务器发送请求界面,然后在请求界面进行数据请求。
基于上述分析,在服务器测添加相关代码,最终代码整合如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#define PORT 8081
#define MAX_SIZE 1024 * 5
// 发送html文件给客户端
void vSendHtmlToCllient(const char *filepath,int new_socket)
{
FILE *file;
char *response_header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\rAccess-Control-Allow-Origin: *\r\nCache-Control: no-cache, no-store, must-revalidate\r\nX-Content-Type-Options: nosniff\r\n\r\n";
// 发送响应头部
send(new_socket, response_header, strlen(response_header), 0);
printf("send:\n%s\n", response_header);
// 读取文件内容并发送
char buffer[1024] = {0};
size_t bytes_read;
file = fopen(filepath, "r");
if (file == NULL)
{
perror("fopen");
exit(EXIT_FAILURE);
}
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0)
{
send(new_socket, buffer, bytes_read, 0);
printf("%s", buffer);
bzero(buffer, sizeof(buffer));
}
}
int main()
{
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char rcvbuffer[MAX_SIZE] = {0};
// 创建TCP套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定套接字到指定端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听端口
if (listen(server_fd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d\n", PORT);
while(1)
{
printf("waiting......\n");
// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0)
{
perror("accept failed");
exit(EXIT_FAILURE);
}
bzero((char *)rcvbuffer, sizeof(rcvbuffer));
int bytesReceived = read(new_socket, rcvbuffer, MAX_SIZE);
printf("bytesReceived = %d\n", bytesReceived);
if (bytesReceived > 0)
{
printf("Received request: \n%s\n", rcvbuffer);
// 只做简单处理
if(strstr(rcvbuffer, "OPTIONS") != NULL)
{
// 构造CORS响应头部
const char* headers = "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate"
"Access-Control-Allow-Methods: POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type\r\n"
"Access-Control-Max-Age: 86400\r\n"
"X-Content-Type-Options: nosniff\r\n"
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
"Content-Length: 0\r\n"
//"Connection: close\r\n"
"\r\n";
printf("send:\n%s", headers);
send(new_socket, headers, strlen(headers), 0);
}
else if(strstr(rcvbuffer, "username") != NULL)
{
const char* json_response = "{\"status\":\"ok\"}";
int json_response_length = strlen(json_response);
// 构造HTTP响应头部
const char* http_version = "HTTP/1.1";
const char* status_code = "200";
const char* status_message = "OK";
const char* access_control_allow_origin = "Access-Control-Allow-Origin: *\r\n";
const char* access_control_allow_Cache = "Cache-Control: no-store, no-cache, must-revalidate\r\n";
const char* content_type = "Content-Type: application/json\r\n";
const char* content_length = "Content-Length: ";
char content_length_header[32];
snprintf(content_length_header, sizeof(content_length_header), "%d", json_response_length);
// 构造完整的HTTP响应
char response[1024] = {0}; // 假设响应不会超过1024字节
snprintf(response, sizeof(response),
"%s %s %s\r\n"
"%s"
"%s"
"%s"
"%s%s\r\n"
"\r\n"
"%s",
http_version, status_code, status_message, access_control_allow_origin, access_control_allow_Cache,
content_type, content_length_header, "\r\n",
json_response);
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
else if(strstr(rcvbuffer, "query1") != NULL)
{
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nHello from server Reply to query1";
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
else if(strstr(rcvbuffer, "query2") != NULL)
{
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nHello from server Reply to query2";
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
else if(strstr(rcvbuffer, "query3") != NULL)
{
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nHello from server Reply to query3";
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
else if(strstr(rcvbuffer, "query4") != NULL)
{
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nHello from server Reply to query4";
printf("send:\n%s\n", response);
send(new_socket, response, strlen(response), 0);
}
else if(strstr(rcvbuffer, "gethtml") != NULL)
{
vSendHtmlToCllient("./Require.html",new_socket);
}
else if(strstr(rcvbuffer, "Login") != NULL)
{
vSendHtmlToCllient("./Login.html",new_socket);
}
}
close(new_socket);
}
close(server_fd);
return 0;
}
登录界面也需要添加登录成功后获取请求界面的相关代码,最终代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
text-align: center;
padding: 20px;
}
h2 {
color: #333;
}
form {
max-width: 300px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
label {
display: block;
text-align: left;
margin-bottom: 5px;
}
input {
width: calc(100% - 10px);
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 3px;
display: inline-block;
}
button {
padding: 8px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<h2>Login</h2>
<form>
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br><br>
<button type="button" onclick="submitForm()">Login</button>
</form>
</body>
<script>
function submitForm() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const url = "http://172.21.247.112:8081/login"; // 服务器地址
// 第一个 fetch 请求
fetch(url, {
method: "POST",
mode: 'cors',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ username: username, password: password })
})
.then(response => {
console.log("第一个请求的响应 json!");
return response.json();
})
.then(data => {
console.log("第一个请求接收到的数据:", data);
if (data.status === 'ok') {
console.log("第一个请求登录成功!");
alert("登录成功!");
fetch("http://172.21.247.112:8081/gethtml")
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(html => {
console.log(html);
// 打开文档以进行写入
document.open();
// 写入从服务器接收到的 HTML 内容
document.write(html);
// 关闭文档
document.close();
})
.catch(error => {
console.error('There was a problem with your fetch operation:', error);
alert("There was a problem with your fetch operation: " + error);
});
} else {
alert("登录失败!");
}
})
.catch(error => {
alert("登录错误!" + error);
});
}
</script>
</html>
请求界面最终代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Query Interface</title>
<style>
body {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
height: 100vh;
margin: 0;
}
h2 {
text-align: center;
}
div {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
margin: 5px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
textarea {
margin-top: 10px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
</style>
</head>
<body>
<h2>Query Interface</h2>
<div>
<button onclick="queryData('query1')">Query 1</button>
<button onclick="queryData('query2')">Query 2</button>
<button onclick="queryData('query3')">Query 3</button>
<button onclick="queryData('query4')">Query 4</button>
</div>
<textarea id="result" rows="10" cols="50" readonly></textarea>
</body>
<script type="text/javascript">
function queryData(query) {
var url = 'http://172.21.247.112:8081/query?q=' + query;
fetch(url)
.then(response => response.text())
.then(data => {
document.getElementById("result").value = data;
})
.catch(error => {
console.error('请求出错:', error);
alert("error!");
});
}
</script>
</html>
登录界面保存文件名称为Login.html,请求界面保存文件名称为Require.html,两个文件和服务器程序放在同一目录下,测试流程大致如下:
测试结果如下:
3、总结
本文是 Linux网络服务器应用实战的服务器和客户端初步调试篇,实现了客户端和服务器的初步通信,通过Web客户端可以和服务器进行简单通信,实现数据的简单收发。