之前实现了单进程版本的TCP网络通信,但实际上单进程版本的网络通信使用较少,更常用的便是多进程与多线程的版本。
多进程方式:
服务器创建子进程,子进程再创建孙子进程,此时子进程便成为二代子进程,孙子进程为一代子进程,让子进程退出后,这样就可以避免僵尸进程的形成。
//server.c,服务器端实现
int Start(char* ip,int port)
{
//使用socket函数建立套接字,打开网卡文件,返回文件描述符
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)//打开失败
{
printf("sock error\n");
exit(2);
}
//建立sockaddr_in类型的结构体,存放套接字的相关信息
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=inet_addr(ip);
local.sin_port=htons(port);
//绑定IP地址和端口,才能被客户端程序正确找到
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
printf("bind error\n");
exit(3);
}
//使服务器端处于监听状态,最多允许5个客户端处于连接等待的状态,多于5个的连接请求直接忽略
if(listen(sock,5)<0)
{
printf("listen error\n");
exit(4);
}
return sock;//返回套接字文件的文件描述符
}
void service(int sock,char* ip,int port)
{
char buf[64];//开辟缓冲区buf
while(1)
{
buf[0]=0;
size_t s=read(sock,&buf,sizeof(buf));//存放从客户端读到的请求到buf中
if(s>0)//实际读到的字节数大于0时,输出请求的内容到屏幕上
{
buf[s]=0;
printf("%s:%d say# %s\n",ip,port,buf);
write(sock,buf,strlen(buf));
}
else if(s==0)//实际读到0字节时,代表客户端已不再发送请求了,输出提示信息
{
printf("client %s quit!\n",ip);
break;
}
else
{
printf("read error!\n");
break;
}
}
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage:%s:ip port\n",argv[0]);
exit(1);
}
int listen_sock=Start(argv[1],atoi(argv[2]));//创建套接字文件,存放套接字(IP+port)
struct sockaddr_in peer;
char ipbuf[64];
while(1)
{
ipbuf[0]=0;
socklen_t len=sizeof(peer);
int new_sock=accept(listen_sock,(struct sockaddr*)&peer,&len);
//当客户端调用coonect函数时,服务器端要调用accept函数接收连接请求
if(new_sock<0)
{
printf("accept error\n");
continue;
}
//将整型转为“点分十进制”的字符串(该函数适于多进程下的服务器端)
inet_ntop(AF_INET,(const void* )&peer.sin_addr,ipbuf,sizeof(ipbuf));
int p=ntohs(peer.sin_port);
printf("get a new connection:[%s:%d]\n",ipbuf,p);
pid_t id=fork();//创建子进程
if(id==0)//子进程不用监听socket,所以关闭该套接字文件
{
close(listen_sock);
if(fork()<0)//在子进程中创建孙子进程
{
exit(0);
}
service(new_sock,ipbuf,p);//进行与客户端间的通信
close(new_sock);
exit(0);
}
else if(id>0)//父进程等到子进程的终止
{
waitpid(id,NULL,0);
}
else
{
printf("fork error!\n");
continue;
}
}
return 0;
}
多进程版本优点:
(1)可以处理多用户的请求
(2)进程具有独立性,所以多进程服务器端的稳定性更好
缺点:
(1)服务器端在接受连接后,才创建子进程,创建子进程需要浪费一定时间,影响性能‘
(2)多进程服务器随着进程数量的增多,CPU的压力会变大,CPU调度的时间增长,也会造成性能的影响
多线程版本
#include <stdio.h>
#include <error.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//1.调用socket函数,创建套接字文件,
//2.调用bind函数,绑定Ip地址 和 端口号
//3.调用listen函数,监听客户端的请求
//开始进行事件循环
// 4、当客户端调用connect函数时,服务器端接收客户端的连接请求
// 5. 处理客户端发来的请求
// a)这里简单的是将从客户端发来的请求读取输出后,再将请求发送给客户端
// b)将客户端的请求显示在标准输出上
// c)再将请求发回给客户端
//多线程服务器可以同时处理多个用户的请求和响应
//需要注意以下几点
//1.线程空间的开辟应该是在堆上开辟的,使用malloc函数开辟,不能是栈上或者全局的
//2.线程资源的回收
//应该采用线程分离,使内核自动回收线程资源,而不能使用线程等待函数(阻塞式等待,不能同时处理
// 多个用户的请求和响应)
//3.属于同一个进程的所有线程,是共享其所属的进程的资源的,因此关闭一个线程就可以了
typedef struct Info//定义结构体存放套接字结构体,文件描述符
{
struct sockaddr_in _peer;
int fd;
}Info;
void * Enter(void * arg)
{
Info * info =(Info *)arg;
sockaddr_in peer =info->_peer;
int new_socket=info->fd;
printf("Entry:%s:%d\n",inet_ntoa(info->_peer.sin_addr),info->fd);
char buf[1024]={0};
//用来处理一次连接。需要循环的处理
while(1)
{
//7.接收来自客户端的请求
ssize_t read_size=read(new_socket,buf,sizeof(buf)-1);
if(read_size<0)
{
continue;
}
if(read_size==0)
{
printf("client discount!\n");
//用来标识用户已经关闭连接
close(new_socket);
return NULL;
}
buf[read_size]='\0';
//8.将客户端的请求显示再标准输出上
printf("peer %s:%d say %s",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buf);
//9.将请求发送给客户端
write(new_socket,buf,read_size);
printf("写会客户端\n");
}
return NULL;
}
void ProcessConnect(int new_socket,sockaddr_in peer)//处理请求
{
printf("参数:%s :%d\n",inet_ntoa(peer.sin_addr),new_socket);
pthread_t pthread;
Info *info=(Info *)malloc(sizeof(Info));//为线程开辟内存空间
info->fd=new_socket;
info->_peer=peer;
printf("准备给线程传参:%s:%d\n",inet_ntoa(info->_peer.sin_addr),info->fd);
pthread_create(&pthread,0,Enter,info);//创建线程
//int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,(void*)(*start_rtn)(void*) ,void *arg);
//第一个参数为指向线程标识符的指针。
//第二个参数用来设置线程属性。
//第三个参数是线程运行函数的起始地址。
//第四个参数是运行函数的参数。
//pthread_create() 在调用成功完成之后返回零。其他任何返回值都表示出现了错误。
//回收线程的资源
pthread_detach(pthread);
return;
}
int main(int argc,char * argv[])
{
//1.检查命令行参数
if(argc!=3)
{
printf("Usage: ./service [IP] [Port]\n");
return 1;
}
//2.调用socket函数创建套接字文件,返回文件描述符fd
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd<0)
{
perror("socket");
return 1;
}
//3.将套接字的相关信息存储在sockaddr_in 类型的结构体addr中
sockaddr_in addr;
addr.sin_family=AF_INET;
socklen_t addr_len=sizeof(addr);
//将点分十进制的IP地址 转化 为32位数字
addr.sin_addr.s_addr=inet_addr(argv[1]);
//将端口号转为网络字节序
addr.sin_port=htons(atoi(argv[2]));
//4.绑定套接字文件,绑定ip地址与端口号
int bind_ret=bind(fd,(sockaddr *)&addr,addr_len);
if(bind_ret<0)
{
perror("bind");
return 1;
}
//5.服务器处于监听状态
int listen_ret=listen(fd,5);
if(listen_ret<0)
{
perror("listen");
return 1;
}
//开始事件循环,不断接收来自客户端的连接请求,处理请求
while(1)
{
//6.接受客户端的连接请求
int new_socket=accept(fd,(sockaddr *)&addr,&addr_len);
//这里的返回值是一个新的 socket 将内核态建立的连接放到用户态执行
//后续信息传输建立在这个socket
if(new_socket<0)
{
perror("accept");
continue;
}
//7.处理一次连接
ProcessConnect(new_socket,addr);
}
close(fd);
return 0;
}