服务端
PS:服务端不会自行停止,只有手动输入任意终止信号后方可终止。
服务端基本逻辑
首先构造套接字、网络地址结构、绑定、侦听。
使用主进程进行侦听,其余处理函数进入循环内进行操作。
构造出子进程实现并发机制,其中循环内不断对太平间信号进行捕获,进行收尸操作。
//基于TCP协议的客户端和服务器
#include<stdio.h>
#include<string.h>
#include<stdlib.h> //atoi
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<signal.h>
#include<sys/wait.h>
#include<errno.h>
//信号处理函数
void diebodys(int signum){
for(;;){
pid_t pid = waitpid(-1,NULL,WNOHANG);
if(pid == -1){
if(errno == ECHILD){
printf("服务器:子进程回收完毕!\n");
break;
}else{
perror("waitpid");
return;
}
}else if(pid == 0){
printf("服务器:子进程阻塞,跳过!\n");
break;
}else{
printf("服务器:回收了%d的僵尸\n",pid);
}
}
}
int main(){
//对子进程进行回收
if(signal(SIGCHLD,diebodys) == SIG_ERR) {
perror("signal");
return -1;
}
//创建套节字
printf("创建套节字\n");
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1){
perror("socket");
return -1;
}
//构造网络地址结构
printf("构造网络地址结构\n");
struct sockaddr_in moiii;
moiii.sin_family = AF_INET;
moiii.sin_port = htons(19993);
moiii.sin_addr.s_addr = INADDR_ANY;
//将套节字和地址结构绑定在一起
printf("套节字与地址结构绑定\n");
if(bind(fd,(struct sockaddr*)&moiii,sizeof(moiii)) == -1){
perror("bind");
return -1;
}
//启动侦听使之成为侦听套节字
printf("启动侦听\n");
if(listen(fd,1024) == -1){
perror("listen");
return -1;
}
for(;;){
//与客户端建立通信
struct sockaddr_in client;
socklen_t clientlen = sizeof(client);
int newfd = accept(fd,(struct sockaddr*)&client,&clientlen);
if(newfd == -1){
perror("accept");
return -1;
}
//业务处理
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0 ){
printf("关闭子进程侦听\n");
close(fd);
for(;;){
char buf[128]= {};
char newbuf[128] = {};
int count = 0;
ssize_t size = read(newfd,buf,sizeof(buf)-1);
if(size == -1){
perror("read");
return -1;
}
if(size == 0){
break;
}
/*
if(read(newfd,buf,sizeof(buf)-1) == -1){
perror("read");
return -1;
}
if(read(newfd,buf,sizeof(buf)-1) == 0){
printf("hhahahahhaha\n");
break;
}
*/
int num = atoi(buf);
printf("客户端发来的数字:%s\n",buf);
for(;;){
if((num%2 == 0) && (num/2 == 0)){
break;
}
else if(num%2 == 1){
num = num/2;
newbuf[count] = '1';
count++;
}else{
num = num/2;
newbuf[count] = '0';
count++;
}
}
newbuf[count+1] = '\0';
for(int left=0,right=count-1;right>left;left++,right--){
char vary = newbuf[left];
newbuf[left] = newbuf[right];
newbuf[right] = vary;
}
printf("处理后二进制:%s\n",newbuf);
if(write(newfd,newbuf,strlen(newbuf)) == -1){
perror("write");
return -1;
}
}
printf("关闭子进程套节字\n");
close(newfd); //关闭子进程套节字.
}
printf("关闭父进程通信套节字\n");
close(newfd);
}
return 0 ;
}
客户端
PS:服务器可能无需终止,但客户端需要做终止操作。
客户端基本逻辑
首先构造套接字、服务端网络地址结构、绑定。
循环内写入接收与发送,终止条件等。
//基于TCP协议的客户端和服务器
#include<stdio.h>
#include<string.h>
#include<stdlib.h> //atoi
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<signal.h>
#include<sys/wait.h>
#include<errno.h>
int main(){
//创建套节字
printf("创建套节字\n");
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1){
perror("socket");
return -1;
}
//服务端地址结构
struct sockaddr_in moiii;
moiii.sin_family = AF_INET;
moiii.sin_port = htons(19993);
moiii.sin_addr.s_addr = inet_addr("服务器IP地址");
//套节字和服务器地址结构连接
if(connect(fd,(struct sockaddr*)&moiii,sizeof(moiii)) == -1){
perror("connect");
return -1;
}
//客户端业务处理
for(;;){
char buf[128] = {};
fgets(buf,sizeof(buf),stdin);
if(strcmp(buf,"#\n") == 0){
break;
}
printf("向服务端发送:%s\n",buf);
if(send(fd,buf,strlen(buf),0) == -1){
perror("send");
return -1;
}
if(recv(fd,buf,sizeof(buf),0) == -1){
perror("recv");
return -1;
}
printf("转换为二进制数字:%s\n",buf);
}
printf("关闭套节字\n");
close(fd);
return 0;
}
注意易错点
read函数的使用
使用read函数时,要时刻提醒它是一个阻塞函数,使用时要注意用法千万不要把主进程阻塞。
其中被注释掉的这部分代码如下:
/*
if(read(newfd,buf,sizeof(buf)-1) == -1){
perror("read");
return -1;
}
if(read(newfd,buf,sizeof(buf)-1) == 0){
printf("hhahahahhaha\n");
break;
}
*/
为什么使用这部分代码会导致客户端关闭后服务端死循环,而使用这部分代码,就可以正常运行:
ssize_t size = read(newfd,buf,sizeof(buf)-1);
if(size == -1){
perror("read");
return -1;
}
if(size == 0){
break;
}
这部分代码的作用是读取客户端发送的数据,并判断是否读取完毕或遇到错误。两种代码的区别在于读取数据的方式和判断读取状态的条件。
在被注释掉的代码中,read函数被调用两次,并且没有将读取到的数据保存到变量 size 中。第一次调用 read 函数用于判断是否读取到数据,如果返回值为 -1,表示读取出错;如果返回值为 0,表示客户端关闭了连接。然后,又调用了一次 read 函数,用于读取数据,但这次读取的返回值并没有被使用。
而在保留的代码中,通过一次 read 函数调用将读取的数据保存到变量 size 中,然后根据 size 的值进行判断。如果 size 为 -1,表示读取出错;如果 size 为 0,表示客户端关闭了连接。这样可以保证读取的数据被正确处理,同时可以正确判断客户端是否关闭连接。
因此,保留的代码能够正常运行,而被注释掉的代码会导致服务端进入死循环,因为它没有正确判断客户端的状态,而是一直尝试读取数据。当客户端关闭连接后,服务端的 read 函数仍然在阻塞等待数据,因此导致死循环。