博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
socket03---小变形,点对点通信聊天
阅读量:4221 次
发布时间:2019-05-26

本文共 5596 字,大约阅读时间需要 18 分钟。

回顾一下上篇中提到的几个常用的结构和函数:

//最常用的ipv4的sockaddrstruct  sockaddr_in{        in_port_t  sin_port;   //2个字节,实为short  int        struct  in_addr  sin_addr;   //4个字节        sa_family_t  sin_family;    //2个字节,short  int        //还剩下填充字节,8个}  总共是16个字节

函数的话还是那几个字节序和地址转换:

uint16_t htons(uint16_t hostshort); //转portuint32_t htonl(uint32_t hostlong);  //转ip//其余两个反过来即是//将cp(点分十进制ip)转换成二进制数字(in_addr型),再存放到inp中,一般我们不使用它int inet_aton(const char* cp,struct in_addr* inp);//将cp转换成二进制数字(in_addr)int_addr_t inet_addr(const char* cp);//将in_addr型转换为点分十进制ipchar* inet_ntoa(struct in_addr in);

回看了下这些结构和函数,咱来稍微改一下上次的程序,模拟一个点对点的聊天通信

这里代码分成一段一段来写,之后整合即可,从下面开始标号:

/* 这一段是后续加进去的,先不看void handler(int sig){    printf("收到一个信号sig:%d\n",sig);    exit(EXIT_SUCCESS);}*///1.首先常用的是初始化server端的套接字,监听int main(void){    int listenfd;    if((listenfd = socket(PF_INET,SOCK_STREAM,0)) < 0)        ERR_EXIT("socket");    //初始化地址    struct sockaddr_in servaddr;    memset(&servaddr,0,sizeof(servaddr)); //清空    servaddr.sin_family = AF_INET;         servaddr.sin_port = htons(5188);    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //这个INADDR_ANY默认值一般都是0.0.0.0    //地址赋完值,可以使用了    int on = 1;    if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)        ERR_EXIT("setsockopt");    //这里提一句:使用setsockopt是为了程序的健壮性,要由tcp/ip的4次握手断开说起(之后会再讨论),这里简要记住就是为了防止服务器在崩溃之后能够立即重启,不必等待通常4次握手最后需要的TIME_WAIT时间    //TIME_WAIT会占用端口,使得无法重启服务器,而且其会持续一段时间,对服务器来说这是不应该的    //绑定地址    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)        ERR_EXIT("bind");    //监听套接字    if(listen(listenfd,SOMAXCONN) < 0)        ERR_EXIT("listen");    //监听了之后这个套接字将变成被动套接字,记住只是这个时刻listen,之后还会改变    //SOMAXCONN指定内核为此套接字排队的最大连接个数}

初始化工作完成之后,就是server端的对请求的操作了,继续写server.c:

//2.继续上面的代码//创建一个对等方的地址,之后要用它来识别客户端struct sockaddr_in peeraddr;socklen_t peerlen = sizeof(peeraddr);int conn;//从已完成连接队列返回第一个连接,如果已完成的连接队列为空,则阻塞在这里,意思就是没人连的时候server就卡在这里不执行下面的代码了//如果有连接,则这函数将返回的信息都存储在了peeraddr中了if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)    ERR_EXIT("accept");printf("the client's ip address is : %s , port is : %d \n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));//如果上面的accept不阻塞,也就是返回了连接,那就开始处理这个连接pid_t pid;pid = fork();if(pid == -1)    ERR_EXIT("fork");if(pid == 0)    //创建子进程成功,进入{    /*    signal(SIGUSR1,handler);    */    char sendbuf[1024] = {
0}; //只有当写入的时候才会“发送”,不然阻塞在这里 while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL) { write(conn,sendbuf,strlen(sendbuf)); //向套接字写入数据 memset(sendbuf,0,sizeof(sendbuf)); } printf("clild closed\n"); exit(EXIT_SUCCESS);}else{ char recvbuf[1024]; while(1) { memset(recvbuf,0,sizeof(recvbuf)); int ret = read(conn,recvbuf,sizeof(recvbuf)); if(ret == -1) ERR_EXIT("read"); else if(ret == 0) { printf("对等端关闭!\n"); break; } fputs(recvbuf,stdout); //将收到的信息打印到标准输出 } printf("parent close!\n"); kill(pid,SIGUSR1); exit(EXIT_SUCCESS);}return 0;

总结一下server端(这里其实可以叫对等端了,因为是两者之间的通信)的过程:

1.首先创建套接字,设定套接字,bind本机地址,监听它
2.之后将创建好的套接字阻塞在接受对等端的连接,一旦有连接进来,打印对方的ip和port信息,并且fork一个子进程来处理连接
3.fork成功,表明子进程和对等方的socket连接上了,在子进程中专门处理要发送数据的情况(这里处理好了server端向client端发送数据的单向聊天)
4.子进程完了,看回父进程,它执行完那个accept就没事干了,那再给它分配一个接受client反传来的消息这个工作,执行一个read操作,如果读到的ret内容为0—说明已经关闭连接,有内容的话就打印到标准输出(这里server能接收数据了)

上面的过程完了,也就说明server端可以接发数据了。

下面看我们的client端:

//client端的初始化套接字和server端基本上完全一样,不同的是将ip地址指定为127.0.0.1,因为我们是单机操作,还有它是主动发起连接方,不需要绑定和监听/* 这一段是后续加进去的void handler(int sig){    printf("收到一个信号sig:%d\n",sig);    exit(EXIT_SUCCESS);}*/int sock;if((sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)        ERR_EXIT("socket");struct sockaddr_in servaddr;memset(&servaddr , 0 , sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(5188);//上面的都一样servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//发起连接请求if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)    ERR_EXXIT("connect");//fork子进程来接受数据pid_t pid;pid = fork();if(pid == -1)    ERR_EXIT("fork");if(pid == 0){    char recvbuf[1024] = {
0}; while(1) { int ret = read(sock,recvbuf,sizeof(recvbuf)); if(ret == -1) ERR_EXIT("read"); else if(ret == 0) { printf("对等方关闭!\n"); break; } fputs(recvbuf,stdout); } close(sock); kill(getppid(),SIGUSR1);}else{ /* signal(SIGUSR1,handler); */ char sendbuf[1024] = {
0}; while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL) { write(sock,sendbuf,strlen(sendbuf)); memset(sendbuf,0,sizeof(sendbuf)); } close(sock);}return 0;

总结一下client端的流程:

1.创建套接字sock,初始化地址(咱们要请求的server端的地址)
2.使用connect向server的地址发送连接请求
3.fork一个子进程出来处理接受数据,因为client是主动请求端,它连上之后必然首先是client先发送消息,所以父进程处理发送,子进程处理接受
4.父子进程是同时运行的,可以同时响应消息,而父进程在你不操作client端时候不走了,也就是阻塞在了fgets这里,同时只有当有数据到来的时候才执行子进程read,阻塞在了read这里。

注意,还有一个地方我们没说道,就是在server和client某一端断开连接,应该是互相都应该断开(可是我们上面这个程序没有),我们使用了一个捕捉信号量的方法来处理这种情况

在某个进程退出的时候,它会返回一个信号量给当前进程,我们需要使用一个信号量来获取到这个信号量,为了好观察,我们将它打印出来

执行上面的函数

void handler(int sig){    printf("收到一个信号sig:%d\n",sig);    exit(EXIT_SUCCESS);}

退出程序,按照之后的逻辑:

如果是server主动关闭(那程序自动退出了),那么client会收到ret == 0,知道对面已经不来数据了,break跳出while(1),下面就是close(sock),发送一个close给server端,同时kill子进程的pid
如果是client端主动关闭(同上,程序退出),那么server端会得到一个ret == 0,知道对面断开连接,break跳出while(1),kill掉父进程pid,server程序再退出。

到这里就得到一个较为完整的点对点的聊天程序。

最后加上需要的头文件和ERR_EXIT(char*)函数即可,前篇里面有。

程序运行的效果图如下,这是由client先发起的聊天:

这里写图片描述

客户端先关闭的话:

这里写图片描述
为什么图中会出现这样的情况?进程结束了还打印recv a sig = 10到屏幕上?
因为这里是先kill的父进程,然后再退出程序,而当时的子进程并没有结束,在它收到SIGUSR1之后,跳到handler函数中,打印错误sig(到标准输出上),之后才退出 子进程程序。

服务器端先关闭的话:

这里写图片描述

总结:

还需要完善,理清楚程序的逻辑很重要

有什么错误还希望指出,代码经过测试可用。

你可能感兴趣的文章
表单元素与提示文字无法对齐的解决方法
查看>>
图片按钮消除边框
查看>>
关于汉字的正则表达式
查看>>
[转载]去除 struts2 标签中的自动生成的布局样式
查看>>
js控制html中的readonly
查看>>
在js中使用动态原型方法定义对象
查看>>
javascript注册事件的四种方式
查看>>
S.O.L.I.D五大原则之SRP(单一职责)
查看>>
自定义Struts2中的ActionErrors
查看>>
windows xp 系统CMD命令大全
查看>>
控制Highcharts中x轴和y轴坐标值的密度
查看>>
xampp下Apache + Tomcat 集群配置的简单介绍(with sticky session)
查看>>
xampp(Apache + Tomcat)与主机的域名绑定
查看>>
增加windows下Tomcat运行时的内存
查看>>
tomcat群集中session共享的几个方案
查看>>
查看windows的开关机日志
查看>>
查找google谷歌北京IP地址的方法
查看>>
chrome的异常Uncaught ReferenceError: xl_chrome_menu is not defined
查看>>
Java不使用web容器,发布WebService应用
查看>>
大运动量的体能训练之后,如何迅速恢复体力?
查看>>