想要实现服务端与客户端一对一的信息传输,我们需要先了解一些基础的结构体与相关函数
目录
相关的基础结构体
1.struct sockaddr
2.struct sockaddr_in
相关的基础函数
1.htons、htonl、ntohs、ntohl 2.inet_pton、inet_ntop函数 3.socket函数
4.bind函数 5.listen函数 6.connect函数
7.accept函数 8.send函数 9.recv函数
本地主机完成简易的一对一服务端
如何获取本地主机的IP地址
代码实现
结果图示
实现网络编程,我们就要来了解两种结构体,叫做网络信息结构体
该结构体由以下成员组成
//头文件:#include 或#include
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
这种结构体是存在缺陷的:sa_data把目标地址和端口信息混在一起了
sockaddr_in解决了sockaddr的缺陷,把 port 和 addr 分开储存在两个变量中
该结构体内由以下成员组成
//头文件:#include
struct sockaddr_in{
sa_family_t sin_family; //地址族,可传AF_INET(表示IPV4)或AF_INET6(表示IPV6)
uint16_t sin_port; //端口号(16位)(注释1)
struct in_addr sin_addr; //IP地址(32位)(注释2)
char sin_zero; //预留未使用
};
struct in_addr{
In_addr_t s_addr; //32位IPv4地址
};
注释①:端口号的赋值如下所示
为什么传入端口号要用到htons函数呢?别着急,这个问题会在本篇博客的第二部分——相关基础函数中讲到
注释②:IP地址的赋值有以下几种方法
struct sockaddr_in server_addr;
为什么要介绍这两种结构体呢?只介绍第二种结构体不就可以了吗?
第一种结构体创造出来的时候,IPV4还没诞生,更别提第二种结构体了,很多函数都时很早之前创造的,用的参数都是第一种结构体
直接介绍这些函数的功能,大家很可能会搞混,我先来告诉大家一些基础概念
IP是用来找主机的,也就是host,IP地址是32位的
端口是用来应用的,端口是存储在网卡中的,端口是16位的,IP地址的位数比端口号长,也就是(IP long 、 port short)
接下来,我来拿一个函数名举个例子,相信大家就能很快记住这些函数的功能了
比如htons:"h"表示的是host、"to"就是to、"n"表示net、"s"表示short,连起来的意思就是" host to net short "
由于网络信息结构体要通过网络发送,这就必然要经过网卡。所以传入端口号就要先用htons函数将其转化为网卡能够识别的形式
相信大家很快就能明白htons函数的功能了,那就是将端口号由主机字节序转换为网络字节序的整数值。
同理,这四个函数的功能就分别是
函数 | 功能 |
htons | 将端口号由主机字节序转换为网络字节序 |
htonl | 将IP地址由主机字节序转换为网络字节序 |
ntohs | 将端口号由网络字节序转换为主机字节序 |
ntohl | 将IP地址由网络字节序转换为主机字节序 |
inet_pton(AF_INET或AFINET6 , 字符串IP,存放大端序IP的地址);
inet_ntop(AF_INET或AFINET6 , 大端序IP地址 , 存放字符串IP的数组起始地址,数组长度);
函数 | 功能 |
inet_pton | 将IP地址由主机字节序转换为网络字节序(字符串IP转大端序IP) |
inet_ntop | 将IP地址由网络字节序转换为主机字节序(大端序IP转字符串IP) |
介绍一下一会会用到的变量:
函数 | 头文件 | 功能 | 返回值 |
int sockfd = socket(int domain , int type , int protocol); |
#include #include |
创建套接字,提供了进程通信的端点 | 成功:返回指向新创建的socket的文件描述符,也就是sockfd(类型位int) 失败:返回 -1 |
PS:socket函数返回一个整型的socket文件描述符,随后的连接建立、数据传输等操作都是通过该socket实现的。socket是应用层与TCP/IP协议族通信的中间软件抽象层。
创建好的套接字中存在两个变量,分别是IP地址与端口号
由于socket创建出来后,IP地址位0.0.0.0(表示本地任意IP),而端口号是完全随机的,每次进程启动,获得的端口号都不一样,别人也就无法通过固定的端口号找到该进程,所以这时候就需要一个函数,来固定IP地址与端口号,也就是bind函数
介绍一下一会会用到的变量:
函数 | 头文件 | 功能 | 返回值 |
int bind(int sockfd , const struct sockaddr *addr , |
#include #include |
设置套接字中的网络信息,也就是设置固定的IP与端口号,一般服务端要做这件事 | 成功:返回 0 失败:返回 -1 , 并设置错误代码 |
PS:listen只有TCP要用,TCP需要连接
介绍一下一会会用到的变量:
函数 | 头文件 | 功能 | 返回值 |
int listen(int sockfd, int backlog); | #include #include |
监听tcp链接事件 | 成功:返回 0 失败:返回 -1 |
介绍一下一会会用到的变量:
函数 | 头文件 | 功能 | 返回值 |
int connect(int sockfd, const struct sockaddr *server_addr, |
#include #include |
向对端发送TCP连接请求,一般客户端要做这件事 如果遇到网络问题可能会阻塞等待连接 |
成功:返回 0 失败:返回 -1 , 并设置错误代码 |
介绍一下一会会用到的变量:
int sockfd;//正在处于监听功能下的套接字的文件描述符
struct sockaddr* client_addr;//储存接受到的客户端的网络信息的结构体
socklen_t* addrlen;//client_addr结构体长度
函数 | 头文件 | 功能 | 返回值 |
int client_sockfd = accept( int sockfd , struct sockaddr* client_addr , socklen_t *addrlen); |
#include #include |
连接属性相同的套接字,并为这个套接字分配一个文件描述符,然后以整个描述符返回 | 成功:返回为其分配的套接字文件描述符, 用于标识该连接套接字,也就是client_sockfd 失败:返回 -1。 |
PS:这个addrlen不能重用,因为系统会根据收到的客户端的网络信息结构体实际大小修改此值,这是变量的大小是一直变化的
介绍一下一会会用到的变量:
函数 | 头文件 | 功能 | 返回值 |
ssize_t send(int sockfd , void *buf , size_t len , int flags); |
#include #include |
向套接字内发送数据 | 成功:返回实际发送的字节数 失败:返回 -1 |
介绍一下一会会用到的变量:
函数 | 头文件 | 功能 | 返回值 |
ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
#include #include |
接收套接字内的数据 | 成功:返回实际接收的字节数 失败:返回 -1 |
(注意:IP地址会根据网络的变化而变化)
我们可以通过在终端界面下输入命令“ip addr”来获取本地主机IPV4地址(注意:这是私有IP,不是公网IP),下图为操作步骤
这就是你的本地主机IPV4地址,比如我的就是192.168.79.128
服务端
/*************************************************************************
> File Name: nan_server.c
> Author: Nan
> Mail: **@qq.com
> Created Time: 2023年10月20日 星期五 13时59分10秒
************************************************************************/
#include
#include
#include
#include
#include
#include
#include
//定义一个开关,用于决定服务器是否开启,默认为开启状态
#define SERVER_SWITCH 1
int main()
{
//1.分别定义服务端与客户端的网络信息结构体
struct sockaddr_in server_addr , client_addr;
bzero(&server_addr , sizeof(server_addr));
bzero(&client_addr , sizeof(client_addr));
//定义一个读写缓冲区与一个存放客户端IP的缓冲区
char rw_buffer[1500];
char client_IP[16];
bzero(rw_buffer , sizeof(rw_buffer));
bzero(client_IP , sizeof(client_IP));
//2.对服务端网络信息结构体进行初始化
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(6060);
//server_addr.sin_addr.s_addr = inet_addr("192.0.0.1");
server_addr.sin_addr.s_addr = inet_addr("本地主机IPV4地址");
//3.创建套接字,该套接字起到监听与传输信息的作用
int server_sockfd = socket(AF_INET , SOCK_STREAM , 0);
if(server_sockfd == -1)
{
perror("server socket call failed!\n");
exit(-1);
}
//4.将IP地址与端口号绑定到监听套接字上
int bind_result = bind(server_sockfd , (struct sockaddr*)&server_addr , sizeof(server_addr));
if(bind_result == -1)
{
perror("server bind call failed!\n");
exit(-1);
}
printf("server wait connect!\n");//日志打印,可帮助理解程序执行逻辑
//5.监听是否有TCP链接
int backlog = 128;
listen(server_sockfd , backlog);
socklen_t addrlen;
int client_sockfd;
while(SERVER_SWITCH)
{
printf("已进入循环!\n");//日志打印,可帮助理解程序执行逻辑
addrlen = sizeof(client_addr);
//6.如果接收成功,返回对应的文件描述符,并执行以下程序
if( (client_sockfd = accept(server_sockfd , (struct sockaddr*)&client_addr , &addrlen)) > 0)
{
printf("accept call success!\n");
//将网络信息结构体中的大端序IP转为字符串IP并放到读写缓冲区中
inet_ntop(AF_INET , &(client_addr.sin_addr.s_addr) , client_IP , sizeof(client_IP));
printf("client_IP = %s\n" , client_IP);//日志打印,帮助检测是否写入IP地址
sprintf(rw_buffer , "Hello , %s , welcome connect nan_server\n" , client_IP);
printf("读写缓冲区中内容为 %s\n" , rw_buffer);//日志打印,帮助检测是否写入要发送的数据
//将读写缓冲区中的内容发送到服务端的套接字中,由套接字向客户端发送数据
send(client_sockfd , rw_buffer , sizeof(rw_buffer) , MSG_NOSIGNAL);
//清空读写缓冲区与存放IP的缓冲区,以供下一次使用
bzero(rw_buffer , sizeof(rw_buffer));
bzero(client_IP , sizeof(client_IP));
}
else if(client_sockfd == -1)
{
perror("accept call failed!\n");
continue;
}
}
close(server_sockfd);
}
客户端
/*************************************************************************
> File Name: nan_client.c
> Author: Nan
> Mail: **@qq.com
> Created Time: 2023年10月20日 星期五 14时40分39秒
************************************************************************/
#include
#include
#include
#include
#include
#include
#include
int main()
{
struct sockaddr_in server_addr;
bzero(&server_addr , sizeof(server_addr));
//定义读写缓冲区
char rw_buffer[1500];
bzero(rw_buffer , sizeof(rw_buffer));
//对服务端网络信息结构体进行初始化
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(6060);
server_addr.sin_addr.s_addr = inet_addr("服务端IPV4地址");
//创建客户端套接字,向服务端发送连接请求
int client_sockfd = socket(AF_INET , SOCK_STREAM , 0);
if(client_sockfd == -1)
{
perror("client socket call failed!\n");
exit(-1);
}
printf("socket call success!\n");
socklen_t addrlen = sizeof(server_addr);
int connect_result = connect(client_sockfd , (struct sockaddr*)&server_addr , sizeof(server_addr));
if(connect_result == -1)//连接失败
{
perror("client connect call failed!\n");
exit(-1);
}
else//连接成功
{
printf("connect call success!\n");
//将服务端发来的内容接收到读写缓冲区中
recv(client_sockfd , rw_buffer , sizeof(rw_buffer) , 0);
printf("服务端发来的信息为:%s\n" , rw_buffer);
printf("1\n");//日志打印,用于判断前面函数是否执行
sleep(15);
}
}
以上就是本篇博客的全部内容了,大家有什么地方没有看懂的话,可以在评论区留言给我,咱要力所能及的话就帮大家解答解答
今天的学习记录到此结束啦,咱们下篇文章见,ByeBye!