【网络】网络编程套接字(一)(2)

简介: 【网络】网络编程套接字(一)(1)

Ⅱ、运行服务器

当服务器初始化完毕后我们就可以启动服务器了,由于服务器是一个永不退出的进程,所以服务器运行以后一定是一个死循环!

读取数据

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
           struct sockaddr *src_addr, socklen_t *addrlen);

功能:

  • 从网络中读取数据。

参数说明:

  • sockfd:创建的套接字对应的文件描述符,表示从该文件描述符索引的文件当中读取数据。
  • buf:读取到的数据的存放位置。
  • len:期望读取数据的字节数。
  • flags:读取的方式。一般设置为0,表示阻塞读取。
  • src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlensrc_addr结构体的长度,返回时此值会被修改为实际读取到的src_addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。

发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

功能:

  • 将数据发送到网络中。

参数说明:

  • sockfd:创建的套接字对应的文件描述符,表示将数据写入该文件描述符索引的文件当中。
  • buf:待写入数据的起始地址。
  • len:期望写入数据的字节数。
  • flags:写入的方式,一般设置为0,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入dest_addr结构体的长度。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

现在服务端通过recvfrom函数读取客户端数据,我们可以先将读取到的数据当作字符串看待,将读取到的数据的最后一个位置设置为’\0’,此时我们就可以将读取到的数据进行输出,同时我们也可以将获取到的客户端的IP地址和端口号也一并进行输出。

需要注意的是,我们获取到的客户端的端口号此时是网络序列,我们需要调用ntohs函数将其转为主机序列再进行打印输出。同时,我们获取到的客户端的IP地址是整数IP,我们需要通过调用inet_ntoa函数将其转为字符串IP再进行打印输出。

// udp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
    USAGE_ERR
};
class UdpServer
{
public:
    UdpServer(uint16_t port)
        :_port(port)
    {
        std::cout << "port : " << _port << std::endl;
    }
    void UdpServerInit()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "create socket fail : " << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl;
        // 2. 填充sockaddr_in结构体
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        // 将主机序列转换为网络序列
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(_port);
        // 3. 绑定IP,端口号
        if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
        {
            std::cerr << "bind fail :" << strerror(errno) << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "bind success !" << std::endl;
    }
    void UdpServerStart()
    {
      // 缓冲区
        char buf[2048];
        // 网络信息结构体
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 死循环不能让服务器退出
        while (true)
        {
            memset(&peer, 0, len);
            // 收取消息
            ssize_t num = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);
            if (num < 0)
            {
                std::cerr << "recvfrom fail !" << std::endl;
                continue;
            }
            else
            {
              // 结尾补上\0,形成C风格字符串
                buf[num] = '\0';
            }
            // 提取客户端的ip和端口号
            std::string peer_ip = inet_ntoa(peer.sin_addr);
            uint16_t peer_port = ntohs(peer.sin_port);
            std::cout << peer_ip << " | " << peer_port << " |# " << buf << std::endl;
            // 发消息
            sendto(_sockfd, buf, strlen(buf), 0, (struct sockaddr*)&peer, len);
        }
    }
    ~UdpServer()
    {
        if (_sockfd > 0)
        {
            close(_sockfd);
        }
    }
private:
    int _sockfd;            // 套接字的文件描述符
    uint16_t _port;         // 端口号
};

我们服务器启动的时候需要指定端口号,所以这里使用了命令行参数。

// udp_server.cpp
#include "udp_server.hpp"
#include <iostream>
#include <memory>
// 使用手册
static void usage(std::string proc)
{
    std::cout << "usage\n\t" << proc << " 端口号" << std::endl;
}
// 命令行参数,必须输入两个参数,一个是程序名,一个是端口号
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    // 提取本地端口号
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> up(new UdpServer(port));
    up->UdpServerInit();
    up->UdpServerStart();
    return 0;
}

程序启动以后我们可以使用netstat -naup显示进程的网络信息。

netstat常用选项说明:

  • -n:直接使用IP地址,而不通过域名服务器。
  • -a: 显示所有连接中的接口信息。
  • -t:显示TCP传输协议的连线状况。
  • -u:显示UDP传输协议的连线状况。
  • -p:显示正在使用Socket的程序识别码和程序名称。

运行结果

查看网络信息

netstat命令显示的信息中:

  • Proto表示协议的类型
  • Recv-Q表示网络接收队列
  • Send-Q表示网络发送队列
  • Local Address表示本地地址,
  • Foreign Address表示外部地址
  • State表示当前的状态
  • PID表示该进程的进程ID
  • Program name表示该进程的程序名称。

其中Foreign Address写成0.0.0.0:*表示任意IP地址、任意的端口号的程序都可以访问当前进程。

Ⅲ、关于客户端的绑定问题

首先,由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要显示的进行IP和端口号的绑定,而客户端不需要显示的进行绑定的,这个绑定的工作由操作系统来进行绑定,当我们调用类似于sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号。

服务器是为了给客户提供服务的,因此服务器必须要让客户知道自己的IP地址和端口号,否则客户端是无法向服务端发起请求的,这就是服务端要进行显示绑定的原因,只有一个进程绑定了端口号之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。

而客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要明确是那个特定的端口号。

一台设备上可以运行很多客户端,例如:B站客户端绑定了8080端口号,那么以后8080端口号就只能给B站客户端使用,如果8080端口号又被淘宝客户端绑定了并且淘宝先启动了,那么B站客户端就无法启动了,因此客户端端口通常是不绑定,由OS动态分配,也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。

Ⅳ、启动客户端

客户端的编写与服务端类似,只不过客户端不需要我们进行绑定工作的,此外作为一个客户端,它必须知道它要访问的服务端的IP地址和端口号,因此在我们启动客户端时中需要引入服务端的IP地址和端口号。

客户端和服务端在功能上是相互补充的,我们上面的服务器是在读取客户端发来的数据然后回发回去,那么这里我们的客户端就应该向服务端发送数据,然后接收服务器回发的数据。

// client.cpp
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
    USAGE_ERR
};
// 使用手册
static void usage(std::string proc)
{
    std::cout << "usage\n\t" << proc << " IP 端口" << std::endl;
}
// 命令行参数,必须输入三个参数,一个是程序名,一个是IP,一个是端口号
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    // 1. 得到服务器的IP和端口
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    // 2. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket fail : " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }
    // 3. 填充server结构体
    struct sockaddr_in server;
    socklen_t len = sizeof(server);
    memset(&server, 0, len);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    server.sin_port = htons(server_port);
    // 4. 业务处理
    std::string message;
    char buf[2048];
    while (true)
    {
        std::cout << "[pan的服务器] :> ";
        getline(std::cin, message);
        // 发送消息
        // 在我们首次调用系统调用发送数据时,OS会随机选择一个端口号 + 自己的IP进行bind
        ssize_t num = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);
        if (num < 0)
        {
            std::cerr << "sendto fail !" << std::endl;
            continue;
        }
        struct sockaddr_in temp;
        socklen_t temp_len = sizeof(temp);
        memset(&temp, 0, temp_len);
        // 收消息
        num = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&temp, &temp_len);
        if (num < 0)
        {
            std::cerr << "recvfrom fail !" << std::endl;
            continue;
        }
        else
        {
            buf[num] = '\0';
        }
        std::cout << "server's message | " << buf << std::endl;
    }
    return 0;
}

Ⅴ、本地测试

现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,现在我们运行服务器时指明端口号为8080,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1地址,服务端的端口号就是8080。

  • 127.0.0.1 :本地环回,表示当前主机的地址,通常用来进行本地通信或测试。

我们要让服务端先运行,然后再让客户端运行,之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出后回发,这时我们在服务端和客户端的窗口都能看到我们输入的内容。

此时我们再用netstat命令查看网络信息,可以看到服务端的端口是8080,客户端的端口是44777。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。

Ⅵ、网络测试

如果你是云服务器,请确保你想使用的端口已经开放,下面是腾讯云的云服务器开放端口的方法:

好了,我们开始进行网络测试:

你可以将此客户端软件给更多的人,让它们都能够连接你的服务器,进行网络通信。

相关文章
|
16天前
|
网络协议 算法 网络性能优化
C语言 网络编程(十五)套接字选项设置
`setsockopt()`函数用于设置套接字选项,如重复使用地址(`SO_REUSEADDR`)、端口(`SO_REUSEPORT`)及超时时间(`SO_RCVTIMEO`)。其参数包括套接字描述符、协议级别、选项名称、选项值及其长度。成功返回0,失败返回-1并设置`errno`。示例展示了如何创建TCP服务器并设置相关选项。配套的`getsockopt()`函数用于获取这些选项的值。
|
16天前
|
网络协议 安全 网络安全
C语言 网络编程(四)常见网络模型
这段内容介绍了目前被广泛接受的三种网络模型:OSI七层模型、TCP五层模型以及TCP/IP四层模型,并简述了多个网络协议的功能与特性,包括HTTP、HTTPS、FTP、DNS、SMTP、TCP、UDP、IP、ICMP、ARP、RARP及SSH协议等,同时提到了ssh的免费开源实现openssh及其在Linux系统中的应用。
|
21天前
|
安全 Java 网络安全
【认知革命】JAVA网络编程新视角:重新定义URL与URLConnection,让网络资源触手可及!
【认知革命】JAVA网络编程新视角:重新定义URL与URLConnection,让网络资源触手可及!
30 2
|
1月前
|
网络协议 Java
一文讲明TCP网络编程、Socket套接字的讲解使用、网络编程案例
这篇文章全面讲解了基于Socket的TCP网络编程,包括Socket基本概念、TCP编程步骤、客户端和服务端的通信过程,并通过具体代码示例展示了客户端与服务端之间的数据通信。同时,还提供了多个案例分析,如客户端发送信息给服务端、客户端发送文件给服务端以及服务端保存文件并返回确认信息给客户端的场景。
一文讲明TCP网络编程、Socket套接字的讲解使用、网络编程案例
|
19天前
|
开发者 图形学 API
从零起步,深度揭秘:运用Unity引擎及网络编程技术,一步步搭建属于你的实时多人在线对战游戏平台——详尽指南与实战代码解析,带你轻松掌握网络化游戏开发的核心要领与最佳实践路径
【8月更文挑战第31天】构建实时多人对战平台是技术与创意的结合。本文使用成熟的Unity游戏开发引擎,从零开始指导读者搭建简单的实时对战平台。内容涵盖网络架构设计、Unity网络API应用及客户端与服务器通信。首先,创建新项目并选择适合多人游戏的模板,使用推荐的网络传输层。接着,定义基本玩法,如2D多人射击游戏,创建角色预制件并添加Rigidbody2D组件。然后,引入网络身份组件以同步对象状态。通过示例代码展示玩家控制逻辑,包括移动和发射子弹功能。最后,设置服务器端逻辑,处理客户端连接和断开。本文帮助读者掌握构建Unity多人对战平台的核心知识,为进一步开发打下基础。
41 0
|
1月前
|
网络协议 Java 关系型数据库
16 Java网络编程(计算机网络+网络模型OSI/TCP/IP+通信协议等)
16 Java网络编程(计算机网络+网络模型OSI/TCP/IP+通信协议等)
69 2
|
2月前
|
缓存 网络协议 Java
(六)网络编程之化身一个请求感受浏览器输入URL后奇妙的网络之旅!
在浏览器上输入一个URL后发生了什么? 这也是面试中老生常谈的话题,包括网上也有大量关于这块的内容。
|
2月前
|
网络协议 Python
告别网络编程迷雾!Python Socket编程基础与实战,让你秒变网络达人!
【7月更文挑战第27天】在网络编程的广阔天地中,Socket编程常被视为一道难关。但用Python这把钥匙,我们可以轻松入门。Socket作为网络通信的基石,在Python中通过`socket`模块封装了底层细节,简化了开发过程。以下是一个基本的TCP服务器与客户端的示例,展示了如何建立连接、收发数据及关闭连接。为了应对实际场景中的并发需求,我们还可以借助多线程技术来提升服务器处理能力。掌握了这些基础知识后,你将逐步揭开网络编程的神秘面纱,踏上编程高手之路!
30 0
|
3月前
|
安全 Java 网络安全
【认知革命】JAVA网络编程新视角:重新定义URL与URLConnection,让网络资源触手可及!
【6月更文挑战第22天】JAVA网络编程中,URL代表统一资源定位符,用于表示网络资源地址。通过`new URL(&quot;address&quot;)`创建URL对象,可解析和访问其组件。URLConnection是与URL建立连接的接口,用于定制HTTP请求,如设置GET/POST、超时及交换数据。
35 1
|
3月前
|
存储 分布式计算 网络协议
什么是网络编程?网络编程的三要素是什么?
在网络通信协议下,不同计算机上运行的程序,进行的数据传输。
45 1