涉及知识点
- 客户端-服务器模式
- 计算机网络协议簇
- Socket 和文件描述符
- ipv4/ipv6
- IP地址和端口
- 大小端转换
- 通讯的整体流程
整体流程
服务器这边:
1.创建套接字的文件描述符
2.指定服务器IP地址和端口
3.绑定ip地址和端口
4.设置监听
5.等待客户端连接
6.创建通信套接字的文件描述符
7.通信:处理接收的信息, 向客户端发送消息
8.通信结束关闭文件描述符
客户端这边
1.创建用于通信的套接字文件描述符
2.连接服务器(指定服务器地址和端口)
3.通信:发送数据和接收数据
4.关闭文件描述符
源代码如下
服务器端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1.创建监听用的套接字文件描述符
int fd = socket(AF_INET, SOCK_STREAM, 0);
// socket()函数用于创建套接字,
// 第一个参数表示使用的协议簇, AF_INET 为IPv4格式,AF_INET6为IPv6格式
// 第二个参数表示 使用哪一种传输方式,分为流式和报文方式.SOCK_STREAM为流式,SOCK_DGRAM为报文式
// 第三个参数表示网络数据交换规则, 根据使用的传送方式不同, 交换规则也不同, 服务器这边指定了某种数据交换规则,那么客户端也需要使用服务器这边的规则才能进行通信,默认写0 流式写0默认式使用tcp 报文式的默认写0使用udp
if(fd == -1)
{
perror("创建监听的socket失败");
return -1;
}
// 2.绑定IP地址和端口
struct sockaddr_in saddr;
// Init the sockaddr_in
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY; // 0 = 0.0.0.0
// 将文件描述符和ip与端口进行绑定
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
// 绑定函数
// 第一个参数 文件描述符
// 第二个参数 IP与端口组成的结构体
// 指定IP和端口组成的结构体的内存大小
if(ret == -1)
{
perror("绑定错误");
return -1;
}
// 3.设置监听
ret = listen(fd, 128);
// 监听函数
// 第一个参数为 监听套接字的文件描述符
// 第二个参数为 最大监听多少个客户端
if(ret == -1)
{
perror("监听错误");
return -1;
}
// 4.等待客户端的连接
// 创建用于通信的套接字
struct sockaddr_in caddr;
int addrlen = sizeof(caddr);
// 接受客户端的连接
int cfd = accept(fd, (struct sockaddr*)&caddr, &addrlen);
// 连接函数
// 第一个参数 监听文件的文件描述符
// 第一个参数 客户端的ip地址和端口组成的结构体
// 第三个参数 分配客户端信息内存的大小(ip地址 + 端口 组成的通信套接字的大小)
if(cfd == -1)
{
perror("连接错误");
return -1;
// 如果连接成功, 输出客户端的ip地址和端口号
char IP[32];
printf("客户端ip地址为: %s, 端口为: %d\n",
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, IP, sizeof(IP)),
ntohs(caddr.sin_port));
// 注意大小端的转换
// const char *inet_ntop(int af, const void *src ,char *dst, socklen_t size);
}
// 5.通信
while(1)
{
char Buff[1024];
int len = recv(cfd, Buff, sizeof(Buff), 0);
// 接收数据函数
// 第一个参数 通信套接字的文件描述符
// 第二个参数 通信的数据包,一块有效的内存
// 第三个参数 通信数据包的大小或者长度
// 第四个参数 标志, 一般不用,默认填0
if(len > 0)
{
printf("客户端的数据: %s\n",Buff);
send(cfd, Buff, len, 0);
// 发送数据函数
// 第一个参数 通信的套接字的文件描述符
// 第二个参数 发送的数据包
// 第三个参数 发送数据包的长度
// 第四个参数 还是默认填0
}else if(len == 0)
{
printf("客户端断开连接\n");
break;
}
else
{
perror("接收错误");
break;
}
}
// 6.关闭文件描述符
close(fd);
close(cfd);
return 0;
}
// 所以 服务器这边创建两个套接字
// 一个用来连接客户端
// 另一个用来通信
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1.创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("创建通信socket失败");
return -1;
}
// 2.连接服务器 绑定服务器的ip和端口
struct sockaddr_in saddr;
// 初始化绑定的ip与端口的结构体
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.1.1", &saddr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1)
{
perror("连接错误");
return -1;
}
int number = 0;
while(1)
{
// 3.通信
char Buff[1024];
sprintf(Buff, "通信中 :, %d \n", number++);
// 发送数据,通信的套接字 数据 实际长度 0
send(fd, Buff, strlen(Buff)+1, 0);
// 这里+1 应为strlen读取不了字符串结尾的符号\0
// 4. 接收数据前先清空数据
memset(Buff, 0 , sizeof(Buff));
// 接收数据
int len = recv(fd, Buff, sizeof(Buff), 0);
if(len > 0)
{
printf("Server say: %s\n", Buff);
}else if(len == 0)
{
printf("连接断开");
break;
}else
{
perror("接收失败");
break;
}
sleep(1);
}
// 5. 关闭文件描述符;
close(fd);
return 0;
}
// 所以客户端只有一个文件描述符,只用来通信
对应的函数
- socket的创建:
int socket(int domain, int type, int protocol);
- 文件描述符与ip端口绑定函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 服务器端监听客户端函数
int listen(int sockfd, int backlog);
- 等待并连接客户端函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 接收数据的函数
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
- 发送数据的函数
ssize_t send(int fd, const void *buf, size_t len, int flags);