本文旨在实现TCP套接字通信的单进程版本,多进程版本,多线程版本
makefile
.PHONY:all
all:tcpServer tcpClient
tcpServer:tcpServer.c
gcc -o $@ $^
tcpClient:tcpClient.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f tcpServer tcpClient
编写服务端程序
创建socket文件–>绑定端口号–>监听连接(底层)–>处理连接,即服务(来自监听的返回的连接)
tcpServer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
typedef struct
{
int sock;
char buf[24];
int port_t;
}net_info_t;
//准备函数
int startup(char *ip,int port )
{
//创建socket文件
int sock=socket(AF_INET,SOCK_STREAM,0);
if( sock<0)
{
printf("socket error\n");
exit(2);
}
//绑定本地端口号和IP
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=inet_addr(ip);
local.sin_port=htons(port);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
printf("bind error\n");
exit(3);
}
//监听socket,监听成功即可以连接
if(listen(sock,5))
{
printf("listen error\n");
exit(4);
}
return sock;
}
//服务函数
void service(int new_sock,char *ip,int port)
{
char buf[124];
while(1)
{
buf[0]=0;
//读取网络中的数据
ssize_t s=read(new_sock,buf,sizeof( buf)-1);
if(s>0)
{
buf[s]=0;
printf(" [%s:%d] say# %s\n",ip,port,buf);
//往网络中写数据
write(new_sock,buf,strlen(buf));
}
else if(s==0)
{
printf("client [%s:%d]quit!\n",ip,port);
break;
}
else
{
printf("read error\n");
break;
}
}
}
//主函数
int main(int argc,char * argv[])
{
if(argc!=3)
{
printf("Usage:%s [iP][port]\n",argv[0]);
return 1;
}
//获取新连接(从底层获取新连接)
int listen_sock=startup(argv[1],atoi(argv[2]));
struct sockaddr_in peer;
//定义一个缓冲区,用于存放ip地址
char ipBuf[24];
while(1)
{
ipBuf[0]=0;
socklen_t len=sizeof(peer);
//获得新连接(从监听套接字中拿连接),提供数据io的服务
int new_sock=accept(listen_sock,(struct sockaddr*)&peer,&len);
if(new_sock<0)
{
}
//将网络中的地址转换成字符串形式
inet_ntop(AF_INET,( const void*)&peer.sin_addr,ipBuf,sizeof(ipBuf));
int port=ntohs(peer.sin_port);
printf(" get a newconect,[%s][ %d]\n",ipBuf,port);
///////////////以下部分为可替换部分//////////////
//1.单进程版本
service(new_sock,ipBuf,port);
//关闭socket文件描述符
close(new_sock);
//////////以上为可替换部分///////////////////////////////////
}
return 0;
}
查看主机中网络服务器如下
编写客户端程序
创建socket文件–>创建连接(connect)
tcpClient.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char*argv[ ])
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if( sock<0)
{
printf("socket error\n");
return 1;
}
//填充服务器信息
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_addr.s_addr=inet_addr(argv[1]);
server.sin_port=htons(atoi(argv[2]));
//连接服务器
if(connect(sock,( struct sockaddr*)&server,sizeof(server))<0)
{
printf("connect error\n");
return 1;
}
char buf[124];
while(1)
{
printf("please Enter#");
fflush(stdout);
//从标准输入读取数据,写进缓冲区
ssize_t s=read(0,buf,sizeof( buf)-1);
if(s>0)
{
buf[s-1]=0;
//判断客户端是否退出
if( strcmp("quit",buf)==0)
{
printf("client quit!\n");
break;
}
//将缓冲区数据写进sock文件
write(sock,buf,strlen(buf));
//将sock文件写进缓冲区
s=read( sock,buf,sizeof( buf)-1);
buf[s]=0;
//输出缓冲区内容
printf("Server# %s\n",buf);
}
}
close(sock);
return 0;
}
在本文中,我采用回环地址测试即ip地址:127.0.0.1,端口随意设置
测试结果:将服务器运行起来,客户端连接服务端,IP地址和端口 号一致。
即可以看到下面图中的运行结果,模拟网络中两个进程通信
缺点:这种单进程服务器,基本就是一种废的服务器。因为该服务器只能为一个客户端服务。只要一个客户连接上该服务器,其他用户即使在同一个局域网上,也会连接不上服务端,只有当当前的客户端退出,其客户端才能连接上他。并且谁能连上也不一定。因为存在多个客户端竞争一个服务端的问题。
为了解决上述问题,我们现在将单进程版本服务器改成多进程服务器。
多进程版本(将上述服务器程序替换部分的源程序替换掉成下列程序即可)
我们采用让子进程进行数据IO服务,父进程监听链接的思路实现多进程版本服务器
但是由于父子进程之间有等待与被等待的关系。如果父进程不等待子进程,子进程就会变成僵尸进程,进而导致内存泄露的严重问题。如果父进程等待子进程,不管是阻塞式等待。还是非阻塞式等待,效果都不是很好。
虽然此时我们会想到用信号SIGCHILD信号去处理子进程。但是实现验证,效果也不是很理想。
所以我们另辟蹊径,考虑到父子进程有等待关系,我们为了避免这个问题。采用父进程创建子进程,然后子进程再创建子进程(我们叫孙子进程),这时让子进程退出。让孙子进程执行数据IO服务。就解决了等待问题。因为爷孙进程之间不存在等待关系,子进程退出后,孙子进程退出后由1号进程领养处理。也解决了僵尸进程问题
(运行结果和多线程结果一样,只在多线程版本展示运行结果)
程序如下:
//创建子进程
pid_t id=fork( );
//子进程
if(id==0)
{
//关闭监听文件描述符
close(listen_sock);
//创建子进程创建孙子进程,让孙子进程提供服务
if((fork)>0)
{
exit(0);
}
service(new_sock,ipBuf,port);
//关闭new_socket文件描述符
close(new_sock);
exit(0);
}
//父进程
else if(id>0)
{
close(new_sock);
//等待子进程
waitpid(id,NULL,0);
}
//创建失败
else
{
//重新获取listen套接字
printf("fork error!\n");
continue;
}
多进程版本
net_info_t* point=(net_info_t*)malloc(sizeof(net_info_t));
if(point==NULL)
{
printf("molloc error\n");
close(new_sock);
continue;
}
/封装的结构体初始化
point->sock=new_sock;
strcpy(point->buf,ipBuf);
point->port_t=port;
pthread_t tid;
//创建新线程
pthread_create(&tid,NULL,thread_service,(void *)point);
//分离线程
pthread_detach(tid);
```
//多线程辅助函数
void* thread_service(void *arg)
{
net_info_t* p=(net_info_t*)arg;
service(p->sock,p->buf,p->port_t);
close(p->sock);
free(p);
}
客户端结果图
服务端结果图
对象 | 性能问题 | 接受客户数量 | CPU压力 | 稳定性 | 是否共享文件描述符 |
---|---|---|---|---|---|
单进程 | 进程数量很少,最多两个,所以性能好。 | 一个 | 压力非常小 | 稳定性很好 | 否 |
多进程 | 创建子进程,耗时长 | 每个进程占用资源多,导致连接客户端数量有限 | 进程增多,CPU压力大,调度器十分忙碌 | 进程具有独立性,所以稳定行好 | 否 |
多线程 | 创建子进程,耗时较短 | 每个进程占用资源多,导致连接客户端数量比进程多很多 | 轻量级线程,占用资源少,CPU处理方便 | 线程共享进程虚拟地址空间,导致稳定性差 | 是 |
进行网络连接,进行网络通信命令如下
(在编写好服务端,客户端程序后执行,其中服务器主机和客户端主机连接同一个局域网)
1.将网络连接设置成桥接模式
2.关闭防火墙
sudo service iptables stop
2 .拷贝客户端程序给目标主机
ping 目标网络IP地址
scp tcpclient.c 目标网络IP地址(同上) 路径
比如
ping 192.168.3.59
scp tvpClient.c 192.168.3.59 ../home
客户主机执行tcpClient.c 程序即可完成多用户通信。