• 学网络光会理论,不会编程?教你快速上手网络套接字编程


    实现一个基于UDP协议的client/server模型。

    服务器(服务端进程)代码:

    //UdpServer.cc
    #include 
    int main(void)
    {
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    首先就是要创建一个套接字,涉及接口socket
    这里使用的是IPv4的协议族,所以第一个参数是AF_INET;
    使用UDP协议创建的套接字类型设为SOCK_DGRAM;
    只涉及到一个协议,所以第三个参数设置为0
    创建成功会打开套接字文件,该接口会返回打开的套接字文件描述符。

    //UdpServer.cc
    #include 
    #include 
    #include 
    
    int main(void)
    {
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    第二步, 绑定IP、端口号、协议等(套接字地址)。

    关于服务器绑定IP的说明:
    对于多IP主机,服务器可能会有多个网卡,也就是会有多个IP地址。
    可以选择用bind绑定其中一个IP,如果服务器成功绑定了这个ip,那么服务器只能接收发送到这个IP上的端口的数据。这种情况的解决:办法绑定地址通配符INADDR_ANY,作用:所有向该服务器发送请求的端口,无论是哪一个网卡(哪一个IP地址)接收到的,该服务器都会响应。

    但是绑定之前,我们需要先将绑定的内容(套接字地址)描述起来。

    //UdpServer.cc
    #include 
    #include 
    #include 
    #include 
    
    int main(void)
    {
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	//描述绑定的套接字地址
    	const uint16_t port = 8080;
    	struct sockaddr_in addr;
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(port);
    	addr.sin_addr.s_addr = INADDR_ANY;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    htons的作用是把主机序列转为网络序列,请自行查阅原因,这里不做解释。

    开始绑定。

    //UdpServer.cc
    #include 
    #include 
    #include 
    #include 
    
    int main(void)
    {
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	//描述绑定的套接字地址
    	const uint16_t port = 8080;
    	struct sockaddr_in addr;
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(port);
    	addr.sin_addr.s_addr = INADDR_ANY;
    	
    	//绑定 + 判断是否绑定成功
    	if(bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    	{
    		std::cout << "bind address fail!" << std::endl;
    		return 2;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    准备工作都已经做好了。服务器就可以开始供应服务,等待客户端的请求了。
    主要是以下三步:

    • 第一步:服务器等待
    • 第二步:客户端请求
    • 第三步:服务器响应客户端请求
    //UdpServer.cc
    #include 
    #include 
    #include 
    #include 
    #include 
    #define NUM 1024
    
    int main(void)
    {
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	//描述绑定的套接字地址
    	const uint16_t port = 8080;
    	struct sockaddr_in addr;
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(port);
    	addr.sin_addr.s_addr = INADDR_ANY;
    	
    	//绑定 + 判断是否绑定成功
    	if(bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    	{
    		std::cout << "bind address fail!" << std::endl;
    		return 2;
    	}
    	
        //给客户端提供服务部分
        bool quit = false;
        char buffer[NUM];
        while(!quit)
        {
            //接收客户端通过网络传过来的数据
            //并打印在标准输出
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            std::cout << "client# " << buffer << std::endl;
    
            //给客户端回应
            std::string respond = "hello";
            sendto(sockfd, respond.c_str(), respond.size(), 0, (struct sockaddr*)&peer, len);
    
        }
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    关于这几行代码的说明
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
    peer和len作为输出型参数,作用是获取发送数据到该服务器的端口的套接字地址和大小。

    服务器就先写到这里。
    来看客户端

    客户端代码
    第一步,也需要创建套接字。因为套接字是网络通信的端点。

    细节在写服务器的时候已经说过了。

    //UdpClient.cc
    #include 
    #include 
    #include 
    
    int main(void)
    {
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    客户端不需要自己显示地绑定!!!
    原因很简单:

    • 对于客户端,即使是多个ip地址,我们只在乎数据能够发出去就OK,不在乎数据从哪一个网卡(哪一个IP地址)发出去。
    • 对于服务端,服务器能够通过接收到的数据,清楚地知道客户端的套接字地址,因此服务器可以响应回去给客户端。

    所以对于客户端的套接字地址绑定除了增加负担,没有较大好处。客户端绑定,一般交给OS处理。


    客户端如果希望向服务器请求服务,那么必须清楚地知道服务器的IP地址、端口号等信息(服务器的套接字地址)
    所以在客户端里需要描述服务器的套接字地址。

    问题来了,客户端怎么知道服务器的套接字地址?
    对于企业给我们的客户端(例如某音app),代码里面已经内置了服务器的套接字地址。
    而我们自己编写的时候可以“随心所欲”,这里采用以命令行的形式将服务器的ip地址、端口号传给客户端。

    //UdpClient.cc
    #include 
    #include 
    #include 
    #include 
    #include 
    #include  //inet_addr接口所在头文件
    
    //命令行格式: ./client server_ip server_port
    int main(int argc, char* argv[])
    {
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	
    	//描述服务器的套接字地址
        struct sockaddr_in serveraddr;
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    关于serveraddr.sin_port = htons(atoi(argv[2]));的说明。
    端口号是一个2字节16位的整数,而argv是字符串,所以使用atoi转为整数。
    htons的作用是把主机序列转为网络序列,请自行查阅原因,这里不做解释。

    可以对传递的命令行参数做一些检查和提示:

    //UdpClient.cc
    #include 
    #include 
    #include 
    #include 
    #include 
    #include  //inet_addr接口所在头文件
    
    void Use()
    {
        std::cout << "命令格式:./client server_ip server_port;请重试" << std::endl;
    }
    // 命令格式:./client server_ip server_port
    int main(int argc, char* argv[])
    {
        if(argc != 3)
        {
            Use();
            return 1;
        }
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	
    	//描述服务器的套接字地址
        struct sockaddr_in serveraddr;
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    客户端的准备工作已经完毕。
    可以开始向服务器请求服务。

    //UdpClient.cc
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include  //inet_addr接口所在头文件
    
    
    void Use()
    {
        std::cout << "命令格式:./client server_ip server_port;请重试" << std::endl;
    }
    // 命令格式:./client server_ip server_port
    int main(int argc, char* argv[])
    {
        if(argc != 3)
        {
            Use();
            return 1;
        }
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	
    	//描述服务器的套接字地址
        struct sockaddr_in serveraddr;
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
        
        //向服务器请求服务
        while(true)
        {
            std::string message;
            std::cout << "请输入->" ;
            std::cin >> message;
    
            //把数据通过网络传送给服务端
            sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    
            //服务端回应,客户端接收
            //接收服务器通过网络传过来的数据,并打印在标准输出
            char buffer[1024];
            struct sockaddr_in tmp; //一个占位符,用于调用recvfrom接口
            socklen_t len = sizeof(tmp);
            recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len);
            std::cout << "server say# " << buffer << std::endl;
        }
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    注意:如果在同一台主机上测试,那么服务器的IP地址使用127.0.0.1

    我放在同一台主机上进行测试的测试结果
    在这里插入图片描述
    以上面的代码写法,是有一些问题的。
    当字符串逐渐变长是没有问题的,但是变短就会出现问题!

    在这里插入图片描述
    因为第二次及以上的输入,是在原来的基础上覆盖。如果上一次的比较长,那么就不会覆盖到后面的。所以解决办法,就是在每一次的结尾处加上"\0"。

    客户端和服务器关于这个问题,代码的优化:
    服务器的优化:

    //UdpServer.cc
    #include 
    #include 
    #include 
    #include 
    #include 
    #define NUM 1024
    
    int main(void)
    {
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	//描述绑定的套接字地址
    	const uint16_t port = 8080;
    	struct sockaddr_in addr;
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(port);
    	addr.sin_addr.s_addr = INADDR_ANY;
    	
    	//绑定 + 判断是否绑定成功
    	if(bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    	{
    		std::cout << "bind address fail!" << std::endl;
    		return 2;
    	}
    	
        //给客户端提供服务部分
        bool quit = false;
        char buffer[NUM];
        while(!quit)
        {
            //接收客户端通过网络传过来的数据
            //并打印在标准输出
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            
    		//会返回接收到的字节个数
            ssize_t cnt = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (cnt > 0)
            {
                buffer[cnt] = '\0';
                std::cout << "client# " << buffer << std::endl;
    
                //给客户端回应
                std::string respond = "hello";
                sendto(sockfd, respond.c_str(), respond.size(), 0, (struct sockaddr *)&peer, len);
            }
            else
            {
                //
            }
        }
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    客户端的优化

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include  //inet_addr接口所在头文件
    
    
    void Use()
    {
        std::cout << "命令格式:./client server_ip server_port;请重试" << std::endl;
    }
    // 命令格式:./client server_ip server_port
    int main(int argc, char* argv[])
    {
        if(argc != 3)
        {
            Use();
            return 1;
        }
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if(sockfd < 0)
    	{
    		//创建失败
    		std::cout << "create socket fail!" << std::endl;
    		return 1;
    	}
    	
    	//描述服务器的套接字地址
        struct sockaddr_in serveraddr;
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
        
        //向服务器请求服务
        while(true)
        {
            std::string message;
            std::cout << "请输入->" ;
            std::cin >> message;
    
            //把数据通过网络传送给服务端
            sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    
            //服务端回应,客户端接收
            //接收服务器通过网络传过来的数据,并打印在标准输出
            char buffer[1024];
            struct sockaddr_in tmp; //一个占位符,用于调用recvfrom接口
            socklen_t len = sizeof(tmp);
            ssize_t cnt = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len);
            if(cnt > 0)
            {
                buffer[cnt] = '\0';
                std::cout << "server回应:" << buffer << std::endl;
            }
        }
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    测试结果
    在这里插入图片描述

  • 相关阅读:
    @PostConstruct虽好,请勿乱用
    ROM是什么? 刷ROM是什么意思?
    leetcode 762. 二进制表示中质数个计算置位
    螺杆支撑座大作用
    Day04JavaWeb第四次笔记---Maven的使用
    yum apt pip conda 国内源
    【校招VIP】“推推”产品项目课程:产品的规划和商业化分析
    PHP FTP 函数
    java object转json格式,转对象存储
    Linux 命令:PS(进程状态)
  • 原文地址:https://blog.csdn.net/qq_56870066/article/details/126571059