介绍
前置基础知识链接在这Socket初体验 C/S模式开发模型_没伞的男孩的博客-CSDN博客
以上链接的CSDN实现的是非并发通信
同一时刻只能有一对主机(客户端与服务端)进行数据交换
本篇CSDN通过调用fork()函数创建父子进程
实现并发的服务器网络通信
在同一时刻 可以有多个客户端主机与一个服务器主机进行数据交换
实现分析
规定
fd0是仅仅用于处理连接请求的socket文件描述符
cfd是用于正式连接后 实现通信以及数据交换的socket文件描述符
在父进程中
使用一个Socket(文件描述符称为fd0)
仅仅用于处理连接的请求
对于子进程的退出 采用信号函数处理的方式回收资源
在子进程中
子进程和父进程共享文件描述符
子进程和父进程不共享堆栈资源(发生写入则会深拷贝 )
在子进程中关闭父进程的fd0
在子进程中使用分别的cfd进行通信
代码(详细注释)
Linux客户端
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 666
//客户端开发
int main()
{
//服务器地址标签
//sockaddr_in 是用于IPV4通信的结构体
//这个结构体里存了协议设定 IP地址 端口号等信息
struct sockaddr_in server_addr;
//客户端连接服务端 TCP协议的固定写法
//AF_INET 和 SOCK_STREAM 是TCP协议的特有参数
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置服务器地址标签
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;//设置协议
//用到一些调整字节序的函数
//inet_pton 将"x.x.x.x"字符串形式的IP地址转化为4字节整形
//inet_pton 自动转化为大端字节序的4字节整形
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
//htons h to n 指的是本地变网络(host to net) 转化为大端字节序
//htons 最后的 s 指的是短整型 s对应两个字节 这里是对端口号的转换
server_addr.sin_port = htons(SERVER_PORT);
//建立C/S连接
//第一个参数是socket的文件描述符
//第二个参数是结构体地址 因历史兼容原因要强制类型转换
//connect函数封装了填写客户端IP地址以及端口号信息的功能
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
//往服务端写入数据
char buf[256] = "";
while(1)
{
read(STDIN_FILENO, buf, sizeof(buf));
write(sockfd, buf, strlen(buf));
int len = read(sockfd, buf, sizeof(buf));
if(len>0)
{
//成功从服务器接收到数据
printf("receive:%s\n", buf);
}
}
return 0;
}
Linux服务端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<string.h>
#include<ctype.h>
#include<arpa/inet.h>
#include<signal.h>
#define SERVER_PORT 666
//信号回调函数 父进程回收子进程
void free_process(int sig)
{
pid_t pid;
//第一参数 等待任意的子进程
//第二参数 子进程退出的状态信息
//第三参数 表示不阻塞 没有子进程退出就立即返回
//写成循环 处理多个子进程要退出的情况
while(1)
{
pid = waitpid(-1, NULL, WNOHANG);
if(pid <= 0)//没有要回收的子进程
{
break;
}
else
{
printf("子进程 pid = %d 终止\n", pid);
}
}
}
//服务端开发
int main(void)
{
//sockaddr_in 是IPV4协议下存储通信地址的结构体
struct sockaddr_in server_addr;
//创建socket 获取文件描述符
int fd0 = socket(AF_INET, SOCK_STREAM, 0);
//初始化结构体标签 写上地址和端口号
bzero(&server_addr,sizeof(server_addr));
//指定协议家族IPV4
server_addr.sin_family = AF_INET;
//htons 是 host to net short
//转换为大端字节序 并且支持短整型 端口号就是两个字节
//一个服务器可能有多个网卡多个IP
//INADDR_ANY 这个宏定义量表示监听所有IP
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
//用结构体标签初始化信箱
//考虑历史兼容性 强制类型转换(struct sockaddr *)
bind(fd0, (struct sockaddr*)&server_addr, sizeof(server_addr));
//启动两条连接队列
//队列中有已完成TCP连接的和未完成TCP连接的
//第二个参数限制总共最多128个请求
listen(fd0, 128);
//等待客户端连接
printf("等待客户端连接\n");
//数据交换
while(1)
{
//客户端的标签 寄信者的信息
struct sockaddr_in client;
socklen_t client_addr_len = sizeof(client);
char client_ip[16] = "";
//创建一个用于数据交换的的socket
//accept函数从已完成TCP连接的队列中获取连接并开始通信
//accept函数返回一个新的socket 有新的文件描述符
//如果没有连接可以提取 accept函数会使代码阻塞
//注意 这个新的socket 和 之前用于连接的socket(fd0) 不是一个文件描述符
int cfd = accept(fd0, (struct sockaddr*)&client, &client_addr_len);
//打印客户端信息
//inet_ntop 讲4字节整形数转化为"x.x.x.x"类型的IP地址
//ntohs net to host short 从网络字节序转为主机字节序
//short 对应占据2字节大小的整形数据 用于表示端口
//获取客户端IP地址
inet_ntop(AF_INET,&client.sin_addr.s_addr,client_ip,sizeof(client_ip));
//获取客户端端口号
int client_port = ntohs(client.sin_port);
printf("client ip:%s port:%d 已连接\n", client_ip, client_port);
//实现并发功能
//调用fork()创建子进程
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("");
exit(0);
}
if(pid ==0)
{
//这是子进程
//关闭用于处理连接的fd0
close(fd0);
char buf[1024] = "";
while(1)
{
int n = read(cfd, buf, sizeof(buf));
if(n < 0)//读数据出错
{
perror("");
close(cfd);
exit(0);
}
if(n == 0)//客户端关闭
{
printf("%s %d 已离线\n",client_ip, client_port);
close(cfd);
exit(0);
}
printf("%s %d : %s\n",client_ip, client_port, buf);
write(cfd, buf, n);
}
}
else
{
//这是父进程
close(cfd);
//回收子进程
//不能用wait() 否则代码阻塞影响提取新的连接
//应当注册信号回调 异步处理 修改信号的处理方式
//SIGCHILD是子进程终止的信号
signal(SIGCHLD, free_process);
}
}
return 0;
}