一、服务器模型
在网络通信中,通常要求一个服务器连接多个客户端
为了处理多个客户端的请求,通常有多种表现形式
1、循环服务器模型
一个服务器可以连接多个客户端,但同一时间只能连接并处理一个客户的请求
socket()
结构体
bind()
listen()
while(1)
{
accept();
while(1)
{
recv()
}
}
2、并发服务器模型
一个服务器可以同时处理多个客户端请求
2.1 多线程
每有一个客户端连接就去创建一个新的线程用于通信
为什么要使用多线程?-->解决服务器不能同时与多个客户通信
什么时间创建新的子线程?-->在accept之后(因为accept可以循环等待与多个客户建立连接,但无法同时与多个客户通信)
主线程 ---->用于循环等待客户的连接
子线程 ----->用于与客户通信
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
// $$ 服务器端 $$
void *handler_thread(void *arg)
{
char buf[128] = "";
int ret;
int acceptfd = *(int *)arg;
while (1)
{
/*接收消息*/
ret = recv(acceptfd, buf, 128, 0); // 0-->相当于read(acceptfd,buf,128)
if (ret < 0)
{
perror("recv err");
return NULL;
}
else if (ret == 0)
{
printf("客户退出\n");
break;
}
else
{
printf("%s 接收成功\n", buf);
memset(buf, 0, sizeof(buf));
}
}
close(acceptfd);
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
/*创建流式套接字*/
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
else
{
printf("创建套接字成功\n");
}
/*指定服务器网络信息 使用的协议族(IPv4-->AF_INET)、IP地址、端口号等*/
// 服务器的网络信息通过一个系统定义好的结构体来描述
struct sockaddr_in saddr; // 定义一个结构体变量
saddr.sin_family = AF_INET; // 确定协议族-->IPv4
saddr.sin_port = htons(atoi(argv[1])); // 确定使用的端口号
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 确定服务器IP地址
/*绑定套接字*/
int t1 = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (t1 < 0)
{
perror("bind err");
return -1;
}
else
{
printf("绑定套接字成功\n");
}
/*监听*/
int t2 = listen(sockfd, 6); // 将默认的主动套接字变为被动套接字
if (t2 < 0)
{
printf("listen err");
return -1;
}
else
{
printf("监听中\n");
}
pthread_t tid;
int acceptfd;
// 定义一个结构体变量来存接收到的客户信息
struct sockaddr_in caddr;
// len是记录客户信息的结构体的大小
int len = sizeof(caddr);
while (1)
{
/*阻塞等待接收客户端的连接请求,并将连接成功的客户端信息写入到结构体变量caddr中*/
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
printf("accept err");
return -1;
}
else
{
printf("等待接收客户端请求\n");
}
printf("客户IP:%s 端口号:%d \n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
pthread_create(&tid, NULL, handler_thread, &acceptfd);
pthread_detach(tid);
}
/* 关闭套接字 */
close(sockfd);
return 0;
}
2.1 多进程
每有一个客户端连接就去创建一个新的进程用于通信
为什么要使用多进程?-->解决服务器不能同时与多个客户通信
什么时间创建新的子进程?-->在accept之后(因为accept可以循环等待与多个客户建立连接,但无法同时与多个客户通信)
主进程 ---->用于循环等待客户的连接
子进程 ----->用于与客户通信
2.3 IO多路复用
创建两个表fd_set rfds, tempfds
清空表 FD_ZREO(&rfds) ; FD_ZREO(&tempfds);
创建流式套接字socket() 得到sockfd
指明网络信息struct sockaddr_in saddr;
绑定套接字bind()
监听listen()
将想要监听的文件描述符放入表中 FD_SET(sockfd,&rfds);FD_SET(0,&rfds);
//设置变量max用来保存文件描述符最大值
while(1){
tempfds=rfds;
//使用select轮询监听tempfds表
select(max+1,&tempfds,......)
//判断是哪个文件描述符发生了变化,并做出相应处理
if(FD_ISSTE(sockfd,&tempfds);)
{
acceptfd=accept();
if(acceptfd>max)
max=acceptfd;
FD_SET(acceptfd,&rfds);
}
for(int i=0;i<max;i++)
{
if(FD_ISSET(acceptfd))
{
//通信
if(出错)
{
return -1;
}
else if(有客户退出)
{
close(i);
FD_CLR(i,&rfds);
while(FD_ISSET(max,&rfds)==0) //看是否需要更新max
max--;
}
else
{
收发消息
}
}
}
}
3、UDP
3.1通信流程
3.2 函数接口
(1) 接收信息recvfrom()
recvfrom的最后连两个参数与accept最后两个参数起到了相同的作用--->获取消息发送方的信息
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:
sockfd:套接字描述符
buf:接收缓存区的首地址
len:接收缓存区的大小
flags:0
src_addr:发送端的网络信息结构体的指针
addrlen:发送端的网络信息结构体的大小的指针
返回值:
成功接收的字节个数
失败:-1
0:客户端退出
(2) 发送信息 sendto()
sendto的最后连两个参数与connect最后两个参数传参一样---->确定消息要发给谁
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据
参数:
sockfd:套接字描述符
buf:发送缓存区的首地址
len:发送缓存区的大小
flags:0
src_addr:接收端的网络信息结构体的指针
addrlen:接收端的网络信息结构体的大小
返回值:
成功发送的字节个数
失败:-1
普通通信:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
/*消息发送方(无bind)*/
int main(int argc, char const *argv[])
{
/*创建数据报套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
else
{
printf("创建套接字成功\n");
}
/*指定服务器网络信息 使用的协议族(IPv4-->AF_INET)、IP地址、端口号等*/
// 服务器的网络信息通过一个系统定义好的结构体来描述
struct sockaddr_in saddr; // 定义一个结构体变量
saddr.sin_family = AF_INET; // 确定协议族-->IPv4
saddr.sin_port = htons(atoi(argv[1])); // 确定使用的端口号
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 确定服务器IP地址
char buf[128]="";
while (1)
{
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1]='\0';
if (strcmp(buf,"quit")==0)
{
break;
}
sendto(sockfd, buf, 128, 0,(struct sockaddr *)&saddr,sizeof(saddr)); // 0-->相当于read(acceptfd,buf,128)
}
return 0;
}
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
/*消息接收方(有bind)*/
int main(int argc, char const *argv[])
{
/*创建数据报套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
else
{
printf("创建套接字成功\n");
}
/*指定服务器网络信息 使用的协议族(IPv4-->AF_INET)、IP地址、端口号等*/
// 服务器的网络信息通过一个系统定义好的结构体来描述
struct sockaddr_in saddr; // 定义一个结构体变量
saddr.sin_family = AF_INET; // 确定协议族-->IPv4
saddr.sin_port = htons(atoi(argv[1])); // 确定使用的端口号
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 确定服务器IP地址
/*绑定套接字*/
int t1 = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (t1 < 0)
{
perror("bind err");
return -1;
}
else
{
printf("绑定套接字成功\n");
}
struct sockaddr_in caddr;
int len=sizeof(caddr);
char buf[128]="";
int ret;
while (1)
{
ret = recvfrom(sockfd, buf, 128, 0,(struct sockaddr *)&caddr,&len); // 0-->相当于read(acceptfd,buf,128)
if (ret < 0)
{
perror("recv err");
return -1;
}
else
{
printf("IP为:%s,端口号为:%d的客户发来了:%s \n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port),buf);
memset(buf, 0, sizeof(buf));
}
}
return 0;
}
4、广播与组播
广播:
● 前面介绍的数据包发送方式只有一个接受方,称为单播
● 如果同时发给局域网中的所有主机,称为广播
● 只有用户数据报(使用UDP协议)套接字才能广播
● 一般被设计成局域网搜索协议
● 广播地址:局域网中主机号最大的一个
注意要在同一网段下!!!
缺点:
广播方式发给所有的主机,过多的广播会大量的占用网络带宽,造成广播风暴,影响正常的通信
广播风暴: 网络长时间被大量的广播数据包所占用,使正常的点对点通信无法正常进行,其外在表现为网络速度奇慢无比,甚至导致网络瘫痪
补充:
int setsockopt(int sockfd,int level,int optname,void *optval,socklen_t optlen)
功能:获得/设置套接字属性
参数:
sockfd:套接字描述符
level:协议层
optname:选项名
optval:选项值
optlen:选项值大小
返回值: 成功 0 失败-1
set:设置 sock:套接字 option:属性 选项
socket属性(int类型,允许则为非0,不允许为0)
流程:
接收者
1. 创建套接字(socket)
2. 指定网络信息
3. 绑定套接字(bind)
4. 接收消息(recvfrom)
5. 关闭套接字(close)
发送者
1. 创建套接字(socket)
2. 由于原本的套接字不支持广播,所以要给套接字设置广播属性
3. 指定网络(服务器)信息
4. 发送消息(sendto)
5. 关闭套接字(close)
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
/*UDP广播*/
int main(int argc, char const *argv[])
{
/*创建数据报套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
else
{
printf("创建套接字成功\n");
}
//为套接字设置广播属性
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval));
//指定网络信息
struct sockaddr_in saddr; // 定义一个结构体变量
saddr.sin_family = AF_INET; // 确定协议族-->IPv4
saddr.sin_port = htons(atoi(argv[1])); // 确定使用的端口号
saddr.sin_addr.s_addr = inet_addr("192.168.50.255"); // IP使用该网段广播地址
//通信
char buf[128] = "";
while (1)
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
if (strcmp(buf, "quit") == 0)
{
break;
}
sendto(sockfd, buf, 128, 0, (struct sockaddr *)&saddr, sizeof(saddr));
}
return 0;
}
组播:
又名多播
● 多播是一个人发送后,只有加入到多播组的人接收数据。
● 多播方式既可以发给多个主机,又能避免像广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)
多播地址:D类IP:224.0.0.1~239.255.255.254
流程
接收者
1. 创建套接字(socket)
2. 设置多播属性,将自己的IP加入到多播组中
3. 指定网络信息
4. 绑定套接字(bind)
5. 接收消息(recvfrom)
6. 关闭套接字(close)
发送者
1. 创建套接字(socket)
2. 指定网络(服务器)信息
3. 发送消息(sendto)
4. 关闭套接字(close)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
/*消息接收方(有bind)*/
int main(int argc, char const *argv[])
{
/*创建数据报套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
else
{
printf("创建套接字成功\n");
}
//设置多播属性,将自己加入多播组(绑定自己的ip和组播ip)
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr=inet_addr(argv[1]);//组播ip
mreq.imr_interface.s_addr=INADDR_ANY;//自己的ip
setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));
/*指定服务器网络信息 使用的协议族(IPv4-->AF_INET)、IP地址、端口号等*/
// 服务器的网络信息通过一个系统定义好的结构体来描述
struct sockaddr_in saddr; // 定义一个结构体变量
saddr.sin_family = AF_INET; // 确定协议族-->IPv4
saddr.sin_port = htons(atoi(argv[2])); // 确定使用的端口号
saddr.sin_addr.s_addr = INADDR_ANY; // 服务器地址
/*绑定套接字*/
int t1 = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (t1 < 0)
{
perror("bind err");
return -1;
}
else
{
printf("绑定套接字成功\n");
}
struct sockaddr_in caddr;
int len=sizeof(caddr);
char buf[128]="";
int ret;
while (1)
{
ret = recvfrom(sockfd, buf, 128, 0,(struct sockaddr *)&caddr,&len);
if (ret < 0)
{
perror("recv err");
return -1;
}
else
{
printf("IP为:%s,端口号为:%d的客户发来了:%s \n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port),buf);
memset(buf, 0, sizeof(buf));
}
}
return 0;
}