回顾一下上篇中提到的几个常用的结构和函数:
//最常用的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(到标准输出上),之后才退出 子进程程序。
服务器端先关闭的话:
总结:
还需要完善,理清楚程序的逻辑很重要
有什么错误还希望指出,代码经过测试可用。