服务端编程初体验

前言:

在上一篇文章中,举例了一个客户端的代码程序,当时是向百度服务器发送请求,然后服务器返回数据给客户端,对于服务端的编程,我们还不太熟悉,所以今天我们就来学习服务端的编程学习!

客户端/服务端编程模式(c/s模式):

如果接触过网络编程的朋友,应该都知道这个c/s模型,他大体的意思是:

  • 服务端长期暴露在网络,并等待客户端连接(这里暴露的方式,就是公开自己的ip地址,这种很容易遭受到恶意攻击,会导致服务端瘫痪,正常的客户端就没法进行使用了!)
  • 客户端发起连接动作,并等待服务端回应

这种模式的特点如下:

  • 服务端无法主动连接客户端
  • 客户端只能按照预定义的方式连接服务端(这里的预定义的的方式指的就是通信协议!而协议就是进行数据交换的规则,并且这种规则是人为定义的!)

下面我们来看一下这种服务端模式的流程步骤和框架图:

  • 准备连接网络
  • 绑定端口
  • 进入端口监听状态(如果这个端口上有连接的话,也就是可以拿到客户端的连接了,那怎么拿到呢?通过accept函数拿到,这个函数的返回值,就是与客户端真正通信的socket值,也就是fd(文件描述符))
  • 等待连接

在这里插入图片描述

相关函数api介绍:

  • 绑定接口(将服务端的socket绑定到一个地址上,这里的地址不仅仅是Ip地址,也包含了端口号!):
int bind(int sock, struct sockaddr *addr , socklen_t addlen);
  • 监听(这里的backlog参数表示队列的长度,意思是多个客户端进行连接的时候,需要进行排队,也就是形成了另一个队列了!通俗的理解,这个参数表示有多少个客户端来连接服务器.):
int listen(int sock, int backlog);
  • 接收(这里主要要注意这个函数的返回值,他返回的是一个与客户端进行通信的socket值!):
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

深度解析服务端:

  • 1、服务端的socket只用于接收和连接,不进行实际通信

  • 2、当接收到连接时,accept()函数返回与客户端与客户端通信的socket;这里我们可以看到socket也是分类型的,一个是实际进行通信的socket,另外一个是接收连接的socket

  • 3、服务端socket用于产生和客户端通信的socket

所以通过上面这三句话的理解,那么你心里会有疑问,socket到底是什么东西呢?如何去理解它呢?为了更好的理解socket,我们从socket()这个函数接口来去找突破口:

  • socket中文意思是“插线板”,这个插线板的话,大家肯定都用过,就是我们平时家里用插座,这个插座可以供你电脑有电还可以供你手机进行充电!简单理解,就是这个插线板可以应对不同的电器来进行充电!

  • 那对于socket来编程的角度来说,这个socket()接口功能也是多功能的;现在我们用这个socket()接口来进行互联网的通信,那么它只能进行互联网的通信嘛?这个不一定哈,它还可以进行专用网络的通信、甚至本地进程之间的通信;所以socket()这个接口的本质就是提供通信的能力;至于哪方面的通信,这个可以由我们自己去决定的,这个就和刚才上面说的这个插线板类似,可以支持不同的电器设备进行充电!

所以socket是什么?主要从以下三个方向来看:

  • 1、从外表来看,socket()就是一个"多功能"函数。在我们编程的话,就直接调用它就行!
  • 2、socket()的返回值是什么?socket()函数的返回值是用于通信的资源标识符;这里我们可以思考一下,我们要进行通信的话,那么是否会占用操作系统的资源呢?答案肯定是会占用的,所以我们要对这些占用的资源,来做一个标识;所以socket()接口返回的是标识占用资源的标识符,且这个标识符是一个整型数值。所以说,在进行网络通信之前,我们要调用socket()接口做准备,做什么准备呢?就是向操作系统申请通信时需要的资源;所以在通信完之后,这个申请的资源就要释放掉,一般我使用close(资源标识符)接口来释放掉之前通信申请的资源。
  • 3、socket()还能做什么?socket()还可以用来进行本地进程间的通信!

服务端代码实战:

1、简单服务端代码实战:

我们先来看一个简单的服务端代码:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    
    
	int server = 0;
	int client = 0;
	struct sockaddr_in saddr = {
    
    0};
	struct sockaddr_in caddr = {
    
    0};
	socklen_t asize = 0;
    
    server = socket(PF_INET,SOCK_STREAM,0);
    if(server == -1)
    {
    
    
       printf("server socket error\n");
       return -1;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(8899);
    if( bind(server,(struct sockaddr*)&saddr,sizeof(saddr) == -1)
    {
    
    
       printf("server bind error\n");
       return -1;
    }
    if( listen(server , 1) == -1)//第二个参数1,表示当前接收一个客户来连接服务端
    {
    
    
       printf("server listen error\n");
       return -1;
    }
   printf("server start success\n");
 
   asize = sizeof(caddr);
   client = accept(server , (struct sockaddr*)&caddr, &asize);
   if(client == -1)
   {
    
    
     printf("client accept error\n");
     return -1;
   }
   printf("client : %d\n",client);

   close(client)
   close(server);

   return 0;
}

在运行这个简单的服务端程序之前,我们先来讲解上面的代码中几个知识点:

  • htonl();他是把将本机字节序转化为网络字节序,也就是转化为大端。
  • INADDR_ANY:监听本机上任何一张网卡上所来的连接;同时它表示的是"0.0.0.0",那么这个值有什么意义呢?意义就在于本机的连接全部接收,这句话是什么意思呢?难道还可以本机的部分连接可以接收,部分连接不可以接收嘛?答案是可以的,这个特殊的ip地址表示只要连接到主机的客户端,通通都接收,我们来看下面的图文解释:
    在这里插入图片描述
  • accept()接口调用之后,将处于阻塞状态,阻塞状态的意思就是一直等待的状态,等到有客户端连接的时候,accept函数才会返回,如果一直没有客户端连接的话,就会一直阻塞这个接口调用的地方,不会向下继续执行代码,也就是整个程序进入了阻塞状态。

下面我们来看一下这个程序的运行:
在这里插入图片描述
我们可以看到,现在服务端程序没有客户端去向它发送连接,所以现在这个程序阻塞在这里,直到有客户端来连接,才会改变这个状态!

所以现在我们来用一下网络调试助手来向服务端发送请求:

在这里插入图片描述
上面只是一个非常简单的服务端程序,实际开发的话,代码量是非常的复杂,所以我们接着在这个服务端程序上,再加点内容上去,让这个服务端程序,可以做更多的事情,比如更进一步的收发数据:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main()
{
    
    
	int server = 0;
	int client = 0;
	struct sockaddr_in saddr = {
    
    0};
	struct sockaddr_in caddr = {
    
    0};
    socklen_t asize = 0;
    int len = 0;
    int r = 0;
    char buffer[32] = {
    
    0};
   
    server = socket(PF_INET,SOCK_STREAM,0);
    if(server == -1)
    {
    
    
       printf("server socket error\n");
       return -1;
    }
   saddr.sin_family = AF_INET;
   saddr.sin_addr.s_addr = htonl(INADDR_ANY);
   saddr.sin_port = htons(8899);
   //进行绑定
   if( bind(server,(struct sockaddr *)&saddr,sizeof(saddr) == -1)
   {
    
    
      printf("server bind error\n");
      return -1;
   }
  //进行监听有多少个客户来连接服务端,这里目前设置了一个客户端来连接服务端
   if(listen(server,1) == -1)
   {
    
    
      printf("server listen error\n");
      return -1;
   }
   //等待客户端的连接,否则一直阻塞在这个accept接口这里,直到真正有客户端连接了,才会改变这个状态
   asize = sizeof(caddr);
   client = accept(server,(struct sockaddr*)&caddr, &asize);
   
   if(client == -1)
   {
    
    
      printf("client accept error\n");
      return -1;
   }
   len = 0;
   do
   {
    
    
      int i = 0;
      r = recv(client,buffer,sizeof(buffer),0);
      //这里判断接收到的字符长度是否大于0
      if(r > 0)
      {
    
    
        len +=r;
      }
      for(i=0;i<r;i++)
      {
    
    
         printf("%c",buffer[i]);
      } 
   }while(len < 64);//这里接收最大的字符长度为64
   
   //现在接收到了客户端发送过来的请求,那么我服务端,就要给客户端发送一些数据了,有求必答
   send(client ,"Hello world",12,0);
   sleep(1);//这里延时一秒的意义子在于send接口能够把数据给发送出去
    
   //释放掉申请的资源
   close(server);
   close(client); 
   return 0;
}

我们来看试验现象:

在这里插入图片描述
现在阻塞在这里,是因为我的客户端没有向服务端发送请求,那么我现在就来发送请求,我这里是用网络调试助手来发送的:

GET /index.html HTTP/1.1
HOST: www.baidu.com
User-Agent: TEST
Connection: close

在这里插入图片描述

最终的效果如下:

在这里插入图片描述
我们可以看到,网络调试助手接收到了服务端发送过来的Hello World了!非常的完美,perfect。

总结:

1、客户端/服务端编程的核心模式:

  • 服务端长时间运行(死循环)接收客户端请求
  • 客户端连接后向服务端发送请求(协议数据)!

猜你喜欢

转载自blog.csdn.net/Dada_ping/article/details/127760750