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

2022-12-29 17:32:26 浏览数 (3)

在使用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保持一样大小
代码语言:javascript复制
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结构体:

代码语言:javascript复制
struct sockaddr_un {
    sa_family_t sun_family;              /* AF_UNIX */
    char        sun_path[UNIX_PATH_MAX]; /* pathname */
};

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

2 编程测试

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

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

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

代码语言:javascript复制
#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,数据报。

代码语言:javascript复制
void UdpClientThread()
{
	int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
	if (sockfd < 0)
	{
		PRINT("create socket failn");
		return;
	}
	PRINT("create socketfd:%dn", 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,数据报。

代码语言:javascript复制
void UdpServerThread()
{
	int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
	if (sockfd < 0)
	{
		PRINT("create socket failn");
		return;
	}
	PRINT("create socketfd:%dn", 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 failn");
		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:%sn", 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,数据流。

代码语言:javascript复制
void TcpClientThread()
{
	//------------socket
	int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (sockfd < 0)
	{
		PRINT("create socket failn");
		return;
	}
	PRINT("create socketfd:%dn", 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 failn");
		return;
	}
	PRINT("connect okn");

	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,数据流。

代码语言:javascript复制
void TcpServerThread()
{
	//------------socket
	int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (sockfd < 0)
	{
		PRINT("create socket failn");
		return;
	}
	PRINT("create socketfd:%dn", 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 failn");
		return;
	}
	PRINT("bind okn");

	//------------listen
	if (listen(sockfd, 5))
	{
		PRINT("listen failn");
		return;
	}
	PRINT("listen okn");

	//------------accept
	int clientfd = accept(sockfd, NULL, NULL);
	PRINT("accept clientfd:%dn", 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:%sn", buf);
			}
			sleep(1);
		}
	}
	PRINT("endn");
}

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

  • 创建socket
  • bind到指定的地址(文件)
  • listen监听TCP客户端的连接请求
  • accept接受TCP客户端的连接
  • recv/read接收TCP客户端的消息

2.3 一种打印技巧

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

printUtil.h

代码语言:javascript复制
#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方式的客户端和服务端程序,分别以独立线程的方式调用起来:

代码语言:javascript复制
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("hellon");
}

测试结果如下:

代码语言:javascript复制
[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通信

1 人点赞