【Linux】如何在本地主机实现简易的一对一服务器(附图解与代码实现)

想要实现服务端与客户端一对一的信息传输,我们需要先了解一些基础的结构体与相关函数

目录

相关的基础结构体

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地址

代码实现

结果图示


相关的基础结构体

实现网络编程,我们就要来了解两种结构体,叫做网络信息结构体

1.struct sockaddr

该结构体由以下成员组成

//头文件:#include 或#include 

struct sockaddr {  
     sa_family_t sin_family;//地址族
    char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
   }; 

这种结构体是存在缺陷的:sa_data把目标地址和端口信息混在一起了

2.struct sockaddr_in

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地址
};

注释①:端口号的赋值如下所示

  • struct sockaddr_in addr;
  • addr.sin_port = htons(目标端口号);

为什么传入端口号要用到htons函数呢?别着急,这个问题会在本篇博客的第二部分——相关基础函数中讲到

注释②:IP地址的赋值有以下几种方法

struct sockaddr_in server_addr;

  1. inet_pton(AF_INET , "服务器IP地址" , &server_addr.sin_addr.s_addr);
  2. server_addr.sin_addr.s_addr = inet_addr("服务器IP地址");
  3. server_addr.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY表示任意IP地址

为什么要介绍这两种结构体呢?只介绍第二种结构体不就可以了吗?

第一种结构体创造出来的时候,IPV4还没诞生,更别提第二种结构体了,很多函数都时很早之前创造的,用的参数都是第一种结构体

相关的基础函数

1.htons、htonl、ntohs、ntohl

直接介绍这些函数的功能,大家很可能会搞混,我先来告诉大家一些基础概念

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地址由网络字节序转换为主机字节序

2.inet_pton、inet_ntop函数

inet_pton(AF_INET或AFINET6 ,  字符串IP,存放大端序IP的地址);

inet_ntop(AF_INET或AFINET6 , 大端序IP地址 , 存放字符串IP的数组起始地址,数组长度);

函数 功能
inet_pton 将IP地址由主机字节序转换为网络字节序(字符串IP转大端序IP)
inet_ntop 将IP地址由网络字节序转换为主机字节序(大端序IP转字符串IP)

3.socket函数

介绍一下一会会用到的变量:

  • int domain;//即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等
  • int type;//创建的套接字的类型,常用的类型有SOCK_STREAM(流式套接字),SOCK_DGRAM(数据报套接字)
  • int protocol;//传0,表示使用默认协议。
函数 头文件 功能 返回值

 int sockfd = socket(int domain ,

int type , int protocol);

#include        
#include
创建套接字,提供了进程通信的端点

 成功:返回指向新创建的socket的文件描述符,也就是sockfd(类型位int)

失败:返回 -1

PS:socket函数返回一个整型的socket文件描述符,随后的连接建立、数据传输等操作都是通过该socket实现的。socket是应用层与TCP/IP协议族通信的中间软件抽象层。

4.bind函数

创建好的套接字中存在两个变量,分别是IP地址与端口号

由于socket创建出来后,IP地址位0.0.0.0(表示本地任意IP),而端口号是完全随机的,每次进程启动,获得的端口号都不一样,别人也就无法通过固定的端口号找到该进程,所以这时候就需要一个函数,来固定IP地址与端口号,也就是bind函数

介绍一下一会会用到的变量:

  • int sockfd;//需要绑定的套接字文件描述符
  • const struct sockaddr* addr;//存入包含网络地址和端口号的网络信息结构体。(PS:ipv4使用的结构体是struct sockaddr_in类型,所以绑定时需要强制类型转换成struct sockaddr类型)(为什么这个地方用的是struct socksddr 结构体呢? 一句话:这个结构体创造出来的时候,IPV4还没诞生,是为了向前兼容)
  • socklen_t addrlen;//addr结构体的长度
函数 头文件 功能 返回值

int bind(int sockfd ,

const struct sockaddr *addr ,
socklen_t addrlen);

#include    
#include
设置套接字中的网络信息,也就是设置固定的IP与端口号,一般服务端要做这件事

成功:返回 0

失败:返回 -1 , 并设置错误代码

5.listen函数

PS:listen只有TCP要用,TCP需要连接

介绍一下一会会用到的变量:

  • int sockfd;//负责监听TCP连接的套接字文件描述符
  • int backlog;//监听序列的大小。监听序列其实是一个双队列结构,一个用于存放刚刚建立3次握手的链接数和,一个用于存放等待建立3次握手队列的链接数和,backlog默认传入128,表示两个队列大小都是128,具体情况如下图所示
函数 头文件 功能 返回值
int listen(int sockfd, int backlog);

#include

#include

监听tcp链接事件

成功:返回 0

失败:返回 -1

【Linux】如何在本地主机实现简易的一对一服务器(附图解与代码实现)_第1张图片

6.connect函数

介绍一下一会会用到的变量:

  • int sockfd;//需要连接的套接字文件描述符
  • const struct sockaddr* server_addr;//存入对端网络地址和端口号的网络信息结构体。
  • socklen_t addrlen;//server_addr结构体的长度
函数 头文件 功能 返回值

int connect(int sockfd,

const struct sockaddr *server_addr,
socklen_t addrlen);

#include

#include

向对端发送TCP连接请求,一般客户端要做这件事

如果遇到网络问题可能会阻塞等待连接

成功:返回 0

失败:返回 -1 , 并设置错误代码

7.accept函数

介绍一下一会会用到的变量:

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不能重用,因为系统会根据收到的客户端的网络信息结构体实际大小修改此值,这是变量的大小是一直变化的

8.send函数

介绍一下一会会用到的变量:

  • int sockfd;//向目标套接字发送数据
  • void* buf;//要发送的数据的首地址
  • size_t len;//要发送多大的数据
  • int flags;//设置为MSG_DONTWAITMSG时,表示非阻塞。设置为MSG_NOSIGNAL,表示禁止send函数向系统发送异常消息(也就是SIGPIPE信号),从而不会被系统杀死。设置为0时 功能和write一样
函数 头文件 功能 返回值

ssize_t send(int sockfd , void *buf ,

size_t len , int flags);

#include

#include

向套接字内发送数据

成功:返回实际发送的字节数

失败:返回 -1

9.recv函数

介绍一下一会会用到的变量:

  • int sockfd;//接收目标套接字内的数据
  • void* buf;//存放数据的地方的首地址
  • size_t len;//要接受多大的数据
  • int flags;//设置为MSG_DONTWAITMSG时,表示非阻塞。设置为MSG_NOSIGNAL,表示禁止recv函数向系统发送异常消息(也就是SIGPIPE信号),从而不会被系统杀死,设置为0时,功能和read一样
函数 头文件 功能 返回值

ssize_t recv(int sockfd, void *buf,

size_t len, int flags);      

#include

#include

接收套接字内的数据

成功:返回实际接收的字节数

失败:返回 -1

本地主机完成简易的一对一服务端

如何获取本地主机的IP地址

(注意:IP地址会根据网络的变化而变化)

我们可以通过在终端界面下输入命令“ip addr”来获取本地主机IPV4地址(注意:这是私有IP,不是公网IP),下图为操作步骤

【Linux】如何在本地主机实现简易的一对一服务器(附图解与代码实现)_第2张图片

这就是你的本地主机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); 
    }
}

结果图示

【Linux】如何在本地主机实现简易的一对一服务器(附图解与代码实现)_第3张图片

以上就是本篇博客的全部内容了,大家有什么地方没有看懂的话,可以在评论区留言给我,咱要力所能及的话就帮大家解答解答

今天的学习记录到此结束啦,咱们下篇文章见,ByeBye!

你可能感兴趣的:(Linux,linux,网络,服务器,学习)