前言:
在上一篇文章中,举例了一个客户端的代码程序,当时是向百度服务器发送请求,然后服务器返回数据给客户端,对于服务端的编程,我们还不太熟悉,所以今天我们就来学习服务端的编程学习!
客户端/服务端编程模式(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、客户端/服务端编程的核心模式:
- 服务端长时间运行(死循环)接收客户端请求
- 客户端连接后向服务端发送请求(协议数据)!