C language to implement a simple web server

Speaking of web servers, the protocol most people think of first is http, then tcp is under http. This article will implement a simple web server through tcp.

This article will focus on explaining how to implement it. The concepts of http and tcp will not be explained in this article.

1. Understand the working principle of Socket and web services

Since it is a web server based on tcp, many friends who learn C language may soon think of sockets. Socket is a relatively abstract communication process, or an abstraction of information interaction between the host and the host. Socket can send data stream into the network, and can also receive data stream.

The information interaction of socket is similar to the reading of local file information on the surface, but the complexity of writing in it is unmatched by local IO, but there are similarities. The interactive steps of socket under win are: WSAStartup initialization --> socket creation socket --> bind binding --> listen listen --> connect connection --> accept receive request --> send/recv send Or receive data --> closesocket close socket --> WSACleanup finally closes.

                                                      

After understanding the basic steps of a socket, let's take a look at the user's general operation of a basic web request. The operation is divided into: open the browser --> enter the resource address ip address --> get the resource. When the target server receives the request generated by the operation, we can regard the server's response process steps as: get request request --> get request key data --> get key data --> send key data. This step of the server's process is to respond after starting the socket for monitoring. Knowing that the request is received through monitoring, use recv to receive the request data, so as to obtain the resource according to the parameter, and finally return the data through send.

Two, create sokect to complete the monitoring

2.1 WSAStartup initialization

First introduce the dependency WinSock2.h in the c language header file:

#include <WinSock2.h>

In the first point, the socket creation steps have been explained. First, the socket initialization operation needs to be completed, and the function WSAStartup is used. The prototype of the function is:

int WSAStartup(
  WORD      wVersionRequired,
  LPWSADATA lpWSAData
);

The parameter wVersionRequired of this function represents the version number of WinSock2; the lpWSAData parameter is a pointer to WSADATA, and the WSADATA structure is used for the information returned after WSAStartup is initialized.

wVersionRequired can be generated using MAKEWORD, here you can use version 1.1 or version 2.2, 1.1 only supports TCP/IP, version 2.1 will have more support, here we choose version 1.1.

First declare a WSADATA structure:

WSADATA wsaData;

Then pass parameters to the initialization function WSAStartup to complete the initialization:

WSAStartup(MAKEWORD(1, 1), &wsaData)

WSAStartup will return a non-zero value if the initialization fails:

if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) 
{
 exit(1);
}

2.2 Create a socket socket

After the initialization is completed, the socket is created. The socket is created and used. The function prototype is:

SOCKET WSAAPI socket(
  int af,
  int type,
  int protocol
);

In the function prototype, af represents the IP address type, PF_INET represents IPV4, and type represents which communication type is used. For example, SOCK_STREAM represents TCP and protocol represents the transmission protocol. Using 0 will use the default value based on the first two parameters.

int skt = socket(PF_INET, SOCK_STREAM, 0);

After the socket is created, if it is -1, it means the creation failed. The judgment is as follows:

if (skt == -1) 
{         
 return -1;
}

2.3 Binding server

After creating the socket, you need to bind the server, configure port information, IP address, etc. First check which parameters are required by the bind function, the function prototype is as follows:

int bind(
  SOCKET         socket,
  const sockaddr *addr,
  int            addrlen
);

The parameter socket represents the bound socket, just pass in the socket; addr is the pointer of the structure variable of sockaddr_in, configure some server information in the structure variable of sockaddr_in; addrlen is the value of addr.

Know the data we need through the bind function prototype, then create a sockaddr_in structure variable to configure the server information:

struct sockaddr_in server_addr;

Then configure the address family to AF_INET corresponding to TCP/IP:

server_addr.sin_family = AF_INET;

Then configure the port information:

server_addr.sin_port = htons(8080);

Then specify the ip address:

server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

If you are not sure about the ip address, you can manually enter it, and finally use the artifact memset to initialize the memory. The complete code is as follows:

//配置服务器 
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
memset(&(server_addr.sin_zero), '\0', 8);

Then use the bind function to bind and determine whether the binding is successful:

//绑定
if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) {       
 return -1; 
} 

2.4 listen for monitoring

After the binding is successful, the port will be monitored. View the listen function prototype:

int listen(
 int sockfd, 
 int backlog
)

In the function prototype, the parameter sockfd represents the socket to be monitored, and the backlog is to set some processing in the kernel (not explained in depth here). Just set it directly to 10, and the maximum upper limit is 128. Use monitoring and determine whether the code is successful:

if (listen(skt, 10) == -1 ) {    
 return -1;
}

The complete code at this stage is as follows:

#include <WinSock2.h>
#include<stdio.h> 
int main(){
 //初始化 
 WSADATA wsaData;
 if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
  exit(1);
 }
 //socket创建 
 int skt = socket(PF_INET, SOCK_STREAM, 0);
 if (skt == -1) {         
  return -1;
 }
 //配置服务器 
 struct sockaddr_in server_addr;
 server_addr.sin_family = AF_INET;
 server_addr.sin_port = htons(8080);
 server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 memset(&(server_addr.sin_zero), '\0', 8);
 //绑定
 if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1){       
  return -1; 
 } 
 //监听 
 if (listen(skt, 10) == -1 ) {    
  return -1;
 }
 
 printf("Listening ... ...\n");
}

Run the code to know that the code has no errors, and output listening:

Insert picture description here

2.5 Get request

After the monitoring is complete, the request is obtained. Restrictions need to use accept to connect to the socket. The accept function prototype is as follows:

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

The parameter sockfd is the specified socket; addr is a pointer to struct sockaddr, generally the client address; addrlen is generally set to sizeof(struct sockaddr_in). The code is:

struct sockaddr_in c_skt; 
int s_size=sizeof(struct   sockaddr_in);
int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);

Next, start accepting client requests and use the recv function. The function prototype is:

ssize_t recv(
 int sockfd, 
 void *buf, 
 size_t len, 
 int flags
)

The parameter sockfd is the communication established by accept; buf is the cache, where the data is stored; len is the size of the cache; flags is generally set to 0:

//获取数据 
char buf[1024];
if (recv(access_skt, buf, 1024, 0) == -1) {
 exit(1);
}

At this point, we add a loop to the outer layer of accpt and recv to make the process repeatable:

while(1){
  //建立连接 
  printf("Listening ... ...\n");
  struct sockaddr_in c_skt; 
  int s_size=sizeof(struct   sockaddr_in);
  int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);
  
  //获取数据 
  char buf[1024];
  if (recv(access_skt, buf, 1024, 0) == -1) {
   exit(1);
  }
 } 

And you can enter 127.0.0.1:8080 in the browser and you will see that the client has printed listening and created a new link:

We add a printf statement to view client requests:

while(1){
  //建立连接 
  printf("Listening ... ...\n");
  struct sockaddr_in c_skt; 
  int s_size=sizeof(struct   sockaddr_in);
  int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);
  
  //获取数据 
  char buf[1024];
  if (recv(access_skt, buf, 1024, 0) == -1) {
   exit(1);
  }
  
  printf("%s",buf);
 } 

Next, we perform corresponding operations on the request header.

2.6 Writing the request processing layer

After getting the request, start writing the processing layer. Continue to write down the code without levels, write a function called req, the function receives the request information and an established connection as parameters:

void req(char* buf, int access_socket) 
{
}

Then first pass the required value in the while loop:

req(buf, access_skt);

Then start to write the req function, first mark the current directory in the req function:

char arguments[BUFSIZ];  
strcpy(arguments, "./");

Then separate the request and the parameters:

char command[BUFSIZ];     
sscanf(request, "%s%s", command, arguments+2);

Then we mark some head elements:

char* extension = "text/html";   
char* content_type = "text/plain";     
char* body_length = "Content-Length: ";

Then get the request parameters, if you get index.html, get the file under the current path:

FILE* rfile= fopen(arguments, "rb");

After obtaining the file, the request is ok, we first return a 200 status:

char* head = "HTTP/1.1 200 OK\r\n";    
int len; 
char ctype[30] = "Content-type:text/html\r\n";   
len = strlen(head);

Then write a sending function send_:

int send_(int s, char *buf, int *len) 
{
 int total;          
 int bytesleft;                                
 int n;
 total=0;
 bytesleft=*len;
 while(total < *len) 
 {
  n = send(s, buf+total, bytesleft, 0);
  if (n == -1) 
  {
   break;
  }
  total += n;
  bytesleft -= n;
 }
 *len = total;          
 return n==-1?-1:0;         
}

The function of the send function is not difficult to repeat here, it is a logic of traversal sending. Then send http response and file type:

send_(send_to, head, &len);
len = strlen(ctype);
send_(send_to, ctype, &len);

After obtaining the description of the requested file, you need to add the header file to #include <sys/stat.h>use fstat, and generate the necessary information to the connected communication:

//获取文件描述
struct stat statbuf;
char read_buf[1024];       
char length_buf[20];
fstat(fileno(rfile), &statbuf);
itoa( statbuf.st_size, length_buf, 10 );
send(client_sock, body_length, strlen(body_length), 0);
send(client_sock, length_buf, strlen(length_buf), 0);

send(client_sock, "\n", 1, 0);
send(client_sock, "\r\n", 2, 0);

Finally send data:

//·数据发送
char read_buf[1024]; 
len = fread(read_buf ,1 , statbuf.st_size, rfile);
if (send_(client_sock, read_buf, &len) == -1) { 
 printf("error!");   
}

Finally visit the address http://127.0.0.1:8080/index.html, get the index.html file data in the current directory, and render it in the browser:

All codes are as follows:

#include <WinSock2.h>
#include<stdio.h> 
#include <sys/stat.h> 

int send_(int s, char *buf, int *len) {
 int total;          
 int bytesleft;                                
 int n;
 total=0;
 bytesleft=*len;
 while(total < *len) 
 {
  n = send(s, buf+total, bytesleft, 0);
  if (n == -1) 
  {
   break;
  }
  total += n;
  bytesleft -= n;
 }
 *len = total;          
 return n==-1?-1:0;         
}

void req(char* request, int client_sock) {   
 char arguments[BUFSIZ];  
 strcpy(arguments, "./");
 
 char command[BUFSIZ];     
 sscanf(request, "%s%s", command, arguments+2);
 
 char* extension = "text/html";   
 char* content_type = "text/plain";     
 char* body_length = "Content-Length: ";
 
 FILE* rfile= fopen(arguments, "rb");
 

 char* head = "HTTP/1.1 200 OK\r\n";    
 int len; 
 char ctype[30] = "Content-type:text/html\r\n";   
 len = strlen(head);
  
 send_(client_sock, head, &len);
 len = strlen(ctype);
 send_(client_sock, ctype, &len);
 

 struct stat statbuf;
       
 char length_buf[20];
 fstat(fileno(rfile), &statbuf);
 itoa( statbuf.st_size, length_buf, 10 );
 send(client_sock, body_length, strlen(body_length), 0);
 send(client_sock, length_buf, strlen(length_buf), 0);

 send(client_sock, "\n", 1, 0);
 send(client_sock, "\r\n", 2, 0);
 

 char read_buf[1024]; 
 len = fread(read_buf ,1 , statbuf.st_size, rfile);
 if (send_(client_sock, read_buf, &len) == -1) { 
  printf("error!");   
 }
 
 return;
}


int main(){
 WSADATA wsaData;
 if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
  exit(1);
 }

 int skt = socket(PF_INET, SOCK_STREAM, 0);
 if (skt == -1) {         
  return -1;
 }

 struct sockaddr_in server_addr;
 server_addr.sin_family = AF_INET;
 server_addr.sin_port = htons(8080);
 server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 memset(&(server_addr.sin_zero), '\0', 8);

 if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) {       
  return -1; 
 } 

 if (listen(skt, 10) == -1 ) {    
  return -1;
 }
 
 while(1){

  printf("Listening ... ...\n");
  struct sockaddr_in c_skt; 
  int s_size=sizeof(struct   sockaddr_in);
  int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);

  char buf[1024];
  if (recv(access_skt, buf, 1024, 0) == -1) {
   exit(1);
  }
  
  req(buf, access_skt);
 } 
 
}

Friends can write more flexible designated resource types, error handling, etc. to complete this demo.

Guess you like

Origin blog.csdn.net/weixin_41055260/article/details/109378263