在使用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结构体:
代码语言: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通信