没有IP和端口号,可以进行socket通信吗?

举报
码农爱学习 发表于 2022/10/25 22:42:34 2022/10/25
【摘要】 在使用socket通信时,无论是本机内部通信,还是两台机器通信,也无论是TCP的方式,还是UDP的方式,一般都要指定IP和端口号。在Linux开发中,如果是同一台设备内部通信,也可以不需要IP和端口号,这就是Unix域socket通信,它实际上是通过文件的方式实现通信,从而不再需要IP和端口号。本篇就来介绍了Unix域socket的使用示例。

在使用socket通信时,无论是本机内部通信,还是两台机器通信,也无论是TCP的方式,还是UDP的方式,一般都要指定IP和端口号。在Linux开发中,如果是同一台设备内部通信,也可以不需要IP和端口号,这就是Unix域socket通信,它实际上是通过文件的方式实现通信,从而不再需要IP和端口号。本篇就来介绍了Unix域socket的使用示例。

Unix域socket和普通的socket使用起来区别不大,也有TCP和UDP两种传输方式,在介绍Unix域socket之前,再来看下TCP和UDP两种模式下的socket通信模型。


1 Unix域socket基础知识

在使用IP和端口号的socket通信中,会用到sockaddr和sockaddr_in结构体,两个结构体一样大,都是16个字节,而且都有family属性,不同的是:

  • sockaddr用其余14个字节来表示sa_data

  • sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero,分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小

include <netinet/in.h>
struct sockaddr {
    unsigned short sa_family;   // 2 bytes address family, AF_xxx
    char           sa_data[14]; // 14 bytes of protocol address
};
 
// IPv4 AF_INET sockets:
struct sockaddr_in {
    short          sin_family;  // 2 bytes e.g. AF_INET, AF_INET6
    unsigned short sin_port;    // 2 bytes e.g. htons(3490)
    struct in_addr sin_addr;    // 4 bytes see struct in_addr, below
    char           sin_zero[8]; // 8 bytes zero this if you want to
};
​
struct in_addr {
    unsigned long s_addr; // 4 bytes load with inet_pton()
};

sockaddr和sockaddr_in包含的数据都是一样的,但他们在使用上有区别:

  • 在赋值时,把类型、ip地址、端口填充sockaddr_in结构体

  • 在bind时,强制转换成sockaddr

使用Uinx域的socket时,也会用到sockaddr,另外,还需要用到sockaddr_un结构体:

struct sockaddr_un {
    sa_family_t sun_family;              /* AF_UNIX */
    char        sun_path[UNIX_PATH_MAX]; /* pathname */
};

类比sockaddr_in,此结构体只有协议类型和路径名。

2 编程测试

本篇的测试实例要实现的功能是Unix域socket的客户端与服务端实现通信,先实现一对一的通信功能,客户端和服务端分别使用一个线程,两者通信成功后,每隔一段时间客户端向服务端发送一条消息。

分别使用UDP和TCP两种方式实现上述功能。

下面先来看下需要用到的头文件和宏定义:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <thread>
#include <string>
#include "printUtil.h"
​
#define UNIX_UDP_SOCKET_ADDR "unixUDP.socket"
#define UNIX_TCP_SOCKET_ADDR "unixTCP.socket"
#define BUF_SIZE 100
​
using namespace std;

2.1 UDP方式

2.1.1 客户端代码

Unix域socket的UDP客户端程序,对照UDP方式的socket通信模型,因为UDP是无连接的,作为客户端,只需要创建一个socket,然后向需要发送的地址调用sento即可发送消息了,代码如下。

需要注意的是,UDP通信时,socket的参数选用SOCK_DGRAM,数据报。

void UdpClientThread()
{
    int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        PRINT("create socket fail\n");
        return;
    }
    PRINT("create socketfd:%d\n", sockfd);
​
    struct sockaddr_un addr;
    memset (&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, UNIX_UDP_SOCKET_ADDR);
​
    while(1)
    {
        static int i = 0;
        std::string str("helloUDP" + std::to_string(++i));
        sendto(sockfd, str.c_str(), str.length(), 0, (struct sockaddr *)&addr, sizeof(addr));
        sleep(1);
    }
}

总结下Unix域socket的UDP客户端程序的流程:

  • 创建socket

  • sendto发送消息给指定地址的UDP服务端

2.1.2 服务端代码

Unix域socket的UDP服务端程序,对照UDP方式的socket通信模型,因为UDP是无连接的,作为服务端,只需要先创建一个socket,然后再绑定到要接收消息的地址上,然后就可以使用recvfrom来接收消息了,代码如下。

需要注意的是,UDP通信时,socket的参数选用SOCK_DGRAM,数据报。

void UdpServerThread()
{
    int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        PRINT("create socket fail\n");
        return;
    }
    PRINT("create socketfd:%d\n", sockfd);
​
    struct sockaddr_un addr;
    memset (&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, UNIX_UDP_SOCKET_ADDR);
​
    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
    {
        PRINT("bind fail\n");
        return;
    }
​
    size_t size = 0;
    char buf[BUF_SIZE] = {0};
    while(1)
    {
        size = recvfrom(sockfd, buf, BUF_SIZE, 0, NULL, NULL);
        //size = read(sockfd, buf, BUF_SIZE);
        if (size > 0)
        {
            PRINT("recv:%s\n", buf);
        }
    }
}

总结下Unix域socket的UDP服务端程序的流程:

  • 创建socket

  • bind到指定的地址(文件)

  • recvfrom/read接收UDP客户端的消息

2.2 TCP方式

2.2.1 客户端代码

Unix域socket的TCP客户端程序,对照TCP方式的socket通信模型,因为TCP是有连接的,作为客户端,需要先创建一个socket,然后连接到要发送消息的地址上(注意需要服务端先建立,TCP客户端才能连接上),连上后,调用send或write就可发送消息了,代码如下。

需要注意的是,TCP通信时,socket的参数选用SOCK_STREAM,数据流。

void TcpClientThread()
{
    //------------socket
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        PRINT("create socket fail\n");
        return;
    }
    PRINT("create socketfd:%d\n", sockfd);
​
    struct sockaddr_un addr;
    memset (&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, UNIX_TCP_SOCKET_ADDR);
​
    sleep(2);//wait server ready
​
    //------------connect
    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
    {
        PRINT("connect fail\n");
        return;
    }
    PRINT("connect ok\n");
​
    while(1)
    {
        static int i = 0;
        std::string str("helloTCP" + std::to_string(++i));
        //------------send
        send(sockfd, str.c_str(), str.length(), 0);
        //write(sockfd, str.c_str(), str.length());
        sleep(1);
    }
}
​

总结下Unix域socket的TCP客户端程序的流程:

  • 创建socket

  • connect连接到指定的地址(文件)

  • send/write发送消息给TCP服务端

2.2.1 服务端代码

Unix域socket的TCP服务端程序,对照TCP方式的socket通信模型,因为TCP是有连接的,作为服务端,需要先创建一个socket,然后绑定到要接收消息的地址上,接下来就是监听TCP客户端的连接,等客户端来连接后,就可以使用recv或read来接收消息了,代码如下。

需要注意的是,TCP通信时,socket的参数选用SOCK_STREAM,数据流。

void TcpServerThread()
{
    //------------socket
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        PRINT("create socket fail\n");
        return;
    }
    PRINT("create socketfd:%d\n", sockfd);
​
    struct sockaddr_un addr;
    memset (&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, UNIX_TCP_SOCKET_ADDR);
​
    //------------bind
    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
    {
        PRINT("bind fail\n");
        return;
    }
    PRINT("bind ok\n");
​
    //------------listen
    if (listen(sockfd, 5))
    {
        PRINT("listen fail\n");
        return;
    }
    PRINT("listen ok\n");
​
    //------------accept
    int clientfd = accept(sockfd, NULL, NULL);
    PRINT("accept clientfd:%d\n", clientfd);
    if (clientfd > 0)
    {
        size_t size = 0;
        char buf[BUF_SIZE] = {0};
        while(1)
        {
            //------------recv
            size = recv(clientfd, buf, BUF_SIZE, 0);
            //size = read(clientfd, buf, BUF_SIZE);
            if (size > 0)
            {
                PRINT("recv:%s\n", buf);
            }
            sleep(1);
        }
    }
    PRINT("end\n");
}

总结下Unix域socket的TCP服务端程序的流程:

  • 创建socket

  • bind到指定的地址(文件)

  • listen监听TCP客户端的连接请求

  • accept接受TCP客户端的连接

  • recv/read接收TCP客户端的消息

2.3 一种打印技巧

为了在打印调试信息时,每条信息能把对应的函数名打印出来,这里写了一个PRINT宏定义来进行打印,可以对原本的printf打印,增加函数名的打印功能。

printUtil.h

#ifndef __PRINTUTIL_H_
#define __PRINTUTIL_H_
​
#define FIRST(...) FIRST_HELPER(__VA_ARGS__, throwaway)
#define FIRST_HELPER(first, ...) first
​
#define REST(...) REST_HELPER(NUM(__VA_ARGS__), __VA_ARGS__)
#define REST_HELPER(qty, ...) REST_HELPER2(qty, __VA_ARGS__)
#define REST_HELPER2(qty, ...) REST_HELPER_##qty(__VA_ARGS__)
#define REST_HELPER_ONE(first)
#define REST_HELPER_TWOORMORE(first, ...) , __VA_ARGS__
#define NUM(...) \
    SELECT_10TH(__VA_ARGS__, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
                TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway)
#define SELECT_10TH(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, ...) a10
​
//自定义打印格式
#define PRINT(...) printf("[%s] " FIRST(__VA_ARGS__), __func__ REST(__VA_ARGS__))
​
#endif


2.4 测试结果

将上述的UDP和TCP方式的客户端和服务端程序,分别以独立线程的方式调用起来:

int main()
{
    unlink(UNIX_UDP_SOCKET_ADDR);
    unlink(UNIX_TCP_SOCKET_ADDR);
​
    thread th1(UdpServerThread);
    thread th2(UdpClientThread);
​
    thread th3(TcpServerThread);
    thread th4(TcpClientThread);
​
    th1.join();
    th2.join();
​
    th3.join();
    th4.join();
​
    PRINT("hello\n");
}

测试结果如下:

[UdpServerThread] create socketfd:3
[TcpServerThread] create socketfd:5
[TcpClientThread] create socketfd:6
[TcpServerThread] bind ok
[TcpServerThread] listen ok
[UdpClientThread] create socketfd:4
[UdpServerThread] recv:helloUDP1
[UdpServerThread] recv:helloUDP2
[TcpClientThread] connect ok
[TcpServerThread] accept clientfd:7
[TcpServerThread] recv:helloTCP1
[UdpServerThread] recv:helloUDP3
[TcpServerThread] recv:helloTCP2
[UdpServerThread] recv:helloUDP4
[TcpServerThread] recv:helloTCP3
[UdpServerThread] recv:helloUDP5
[TcpServerThread] recv:helloTCP4
[UdpServerThread] recv:helloUDP6
[TcpServerThread] recv:helloTCP5
[UdpServerThread] recv:helloUDP7
[TcpServerThread] recv:helloTCP6
[UdpServerThread] recv:helloUDP8
[UdpServerThread] recv:helloUDP9
[TcpServerThread] recv:helloTCP7

另外,程序运行后,会在本地生成对应的Unix域socket通信文件,本例程序中,就会生成unixUDP.socket和unixTCP.socket这两个文件。

3 总结

本篇介绍了Unix域的Socket通信实例,包括UDP和TCP两种方式,使用流程总结下来如下图:

使用Unix域的Socket通信(同一台机器内部通信),不再需要IP和端口号,只需要指定一个文件即可实现sokect通信

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。