socket03---小变形,点对点通信聊天

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

//最常用的ipv4的sockaddr
struct  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); //转port
uint32_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型转换为点分十进制ip
char* 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(到标准输出上),之后才退出 子进程程序。

服务器端先关闭的话:
这里写图片描述

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值