创建一个socket 需要几个步骤
服务端: socket -> bind -> listen -> accept -> read/write -> close
客户端: socket -> connect -> read/write -> close
第一个参数为协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL**(或称AF_UNIX,Unix域socket,这个是我们的讲解重点)**、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址
第二个参数 指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET
第三个参数为 指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
int socketFd = socket(SOCKET_DOMIAN, SOCKET_TYPE, SOCKET_PROTOCOL);
bind()函数就是将给这个描述字绑定一个名字。addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。addrlen:对应的是地址的长度。
int bindResult = bind(socketFd, (struct sockaddr *)&serviceAddr, serviceLen);
地址为抽象和文件类型。文件类型会生成一个文件在本地,抽象需要在地址前面加0,对应会生成@开头。可以用如下命令查询查看
netstat -an | grep 地址名称
设置监听的最大连接个数
int listenerResult = listen(socketFd, SOCKET_NUMBER);
传入客户端的对象 和长度 由内核写入。等待客户端的连接。
int connectFd = accept(socketFd, (struct sockaddr *)&clientAddr, &clintLen);
通过 read 或者 write 函数跟客户端通信
int dataSize = read(connectFd, &buffer, sizeof(buffer));
if (dataSize == 0)
{
printf("客户端关闭连接\n");
} else if (dataSize == -1)
{
printf("读取异常\n");
exit(1);
} else {
printf("客户端发送了: %s\n" ,buffer);
for (int i = 0; i < sizeof(buffer); i++)
{
buffer[i] = toupper(buffer[i]);
}
write(connectFd, buffer, sizeof(buffer));
}
int serviceFd = socket(SOCKET_DOMIAN, SOCKET_TYPE, SOCKET_PROTOCOL);
发起对服务端的连接,连接成功后 服务端 accept 方法将继续执行
int connectResult = connect(serviceFd, (struct sockaddr *)&clientAddr, clientLen);
int wirteResult = write(serviceFd, winBuffer, sizeof(winBuffer));
printf("写入服务端数据 %i", wirteResult);
// 阻塞读取服务端数据
int readResult =read(serviceFd, winBuffer, sizeof(winBuffer));
close(serviceFd);
服务端
#include
#include
#include
#include
// scoket相关
#include
#include
#include
#include
// 协议域
int SOCKET_DOMIAN = AF_UNIX;
// 类型
int SOCKET_TYPE = SOCK_STREAM;
// 协议,常用tcp udp
int SOCKET_PROTOCOL = 0;
// SOCKETD地址
#define SOCKET_SERVICE_ADDRESS "natvie_socket_test"
// 允许连接的长度
#define SOCKET_NUMBER 20
// 读取数据大小
#define BUFFER_SZIE 80
/**
* 主文件入口
*/
int main (void) {
// socket 文件描述符
int socketFd;
// 客户端的socket 文件描述符
int clintSocetFd;
// socket 实体类
struct sockaddr_un serviceAddr, clientAddr;
// 地址长度
socklen_t serviceLen;
socklen_t clintLen;
// 数据读取
char buffer[BUFFER_SZIE];
// 创建socket
socketFd = socket(SOCKET_DOMIAN, SOCKET_TYPE, SOCKET_PROTOCOL);
if (socketFd == -1)
{
printf("创建 socket 失败\n");
exit(1);
}
// 清空
memset(&serviceAddr, 0 ,sizeof(sockaddr_un));
serviceAddr.sun_family = AF_UNIX;
// 设置地址
strncpy(serviceAddr.sun_path, SOCKET_SERVICE_ADDRESS, sizeof(serviceAddr.sun_path) - 1);
// 计算长度
serviceLen = sizeof(serviceAddr.sun_family) + sizeof(serviceAddr.sun_path);
// 绑定地址
int bindResult = bind(socketFd, (struct sockaddr *)&serviceAddr, serviceLen);
if (bindResult == -1)
{
printf("绑定 socket 失败\n");
exit(1);
}
// 绑定成功后 需要监听
int listenerResult = listen(socketFd, SOCKET_NUMBER);
if (listenerResult == -1)
{
printf("监听 socket 失败\n");
exit(1);
}
printf("服务端 socket建立 等待连接\n");
// 死循环等待连接
while (1)
{
// 等待接收客户端连接, 同时客户端的信息会写入该类, 会阻塞在这里
clintLen = sizeof(struct sockaddr_un);
int connectFd = accept(socketFd, (struct sockaddr *)&clientAddr, &clintLen);
if (connectFd == -1)
{
printf("监听 socket 失败\n");
exit(1);
}
// 创建成功,循环读取消息
while (1)
{
// 0 为关闭 -1 为异常
int dataSize = read(connectFd, &buffer, sizeof(buffer));
if (dataSize == 0)
{
printf("客户端关闭连接\n");
} else if (dataSize == -1)
{
printf("读取异常\n");
exit(1);
} else {
printf("客户端发送了: %s\n" ,buffer);
for (int i = 0; i < sizeof(buffer); i++)
{
buffer[i] = toupper(buffer[i]);
}
write(connectFd, buffer, sizeof(buffer));
}
}
}
// 退出for循环 关闭socket
printf("服务端关闭连接\n");
close(socketFd);
return 0;
}
客户端
#include
#include
#include
// scoket相关
#include
#include
#include
#include
// 协议域
int SOCKET_DOMIAN = AF_UNIX;
// 类型
int SOCKET_TYPE = SOCK_STREAM;
// 协议,常用tcp udp
int SOCKET_PROTOCOL = 0;
// SOCKETD地址
#define SOCKET_SERVICE_ADDRESS "natvie_socket_test"
// 允许连接的长度
#define SOCKET_NUMBER 20
// 读取数据大小
#define BUFFER_SZIE 80
// 输入最大行
#define MAXLINE 80
int main(void) {
// 客户端操作符
int serviceFd;
sockaddr_un clientAddr;
// 地址长度
socklen_t clientLen;
// 从输入框输入流
char winBuffer[BUFFER_SZIE];
serviceFd = socket(SOCKET_DOMIAN, SOCKET_TYPE, SOCKET_PROTOCOL);
if (serviceFd == -1)
{
printf("创建 socket 失败");
exit(0);
}
// 清空数据
memset(&clientAddr, 0, sizeof(struct sockaddr_un));
// 赋值地址
clientAddr.sun_family = AF_UNIX;
strncpy(clientAddr.sun_path, SOCKET_SERVICE_ADDRESS, sizeof(clientAddr.sun_path) - 1);
clientLen = sizeof(clientAddr);
printf("socket地址: %s\n", clientAddr.sun_path);
int connectResult = connect(serviceFd, (struct sockaddr *)&clientAddr, clientLen);
if (connectResult == -1)
{
perror("连接 socket 失败");
exit(0);
}
printf("请输入对应的数据:");
while (fgets(winBuffer, MAXLINE, stdin))
{
// 把读到的数据给服务端
int wirteResult = write(serviceFd, winBuffer, sizeof(winBuffer));
printf("写入服务端数据 %i", wirteResult);
// 阻塞读取服务端数据
int readResult =read(serviceFd, winBuffer, sizeof(winBuffer));
if (readResult <= 0) {
printf("服务端异常或者已关闭\n");
break;
}else {
printf("接收到服务端的消息: %s \n",winBuffer);
}
printf("请输入对应的数据:");
}
close(serviceFd);
return 0;
}
上面提到的socket 的 accept 和 read 等方法都是阻塞方法,等多客户端无法实现实时的读取和监听,如果每个客户端开对应的线程则比较耗资源。因此有了Epoll 等待唤醒。Epoll 有如下方法。
需要传入一个数量,该数量大于0即可。并会返回一个文件描述符
// 创建 epoll, 参数size 实际没作用,但是要大于0
int epollFd = epoll_create(EPOLL_SZIE);
第二参数控制 添加还是删除
第三个参数为需要监听的文件操作符
第四个参数为 epoll_event 类型
data.fd 可以存储对应的文件ID
events属性表示数据类型 EPOLLIN
int addResult = epoll_ctl(epollFd, EPOLL_CTL_ADD, serviceFd, &epollEvent);
events参数传入事件数组,内核会对应写入的数组。可通过 events.data.fd 获取对应的客户端。
size 表示每次可以处理的最大事件数量
最后参数代表等待的事件,0为马上返回,-1为阻塞等待。
int eventNum = epoll_wait(epollFd, events, EPOLL_EVENT_MAX_SZIE, EPOLL_TIEM_OUT);
#include
#include
#include
#include
// scoket相关
#include
#include
#include
#include
// epoll 相关
#include
// 协议域
int SOCKET_DOMIAN = AF_UNIX;
// 类型
int SOCKET_TYPE = SOCK_STREAM;
// 协议,常用tcp udp
int SOCKET_PROTOCOL = 0;
// SOCKETD地址
#define SOCKET_SERVICE_ADDRESS "epoll_socket_test"
// 允许连接的长度
#define SOCKET_NUMBER 20
// 读取数据大小
#define BUFFER_SZIE 80
// epoll大小
#define EPOLL_SZIE 80
// epoll消息大小
#define EPOLL_EVENT_MAX_SZIE 20
// epoll消息超时
#define EPOLL_TIEM_OUT -1
// 采用抽象作用域
int main(void) {
int serviceFd;
sockaddr_un serviceAddr;
socklen_t serviceLen, clientLen;
char buffer[BUFFER_SZIE];
serviceFd = socket(SOCKET_DOMIAN, SOCKET_TYPE, SOCKET_PROTOCOL);
if (serviceFd == -1)
{
perror("创建 socket 失败");
exit(0);
}
memset(&serviceAddr, 0 , sizeof(struct sockaddr) - 1);
serviceAddr.sun_family = AF_UNIX;
// 这里采用抽象的协议, 第一位要给0
serviceAddr.sun_path[0] = 0;
strcpy(serviceAddr.sun_path + 1, SOCKET_SERVICE_ADDRESS);
// serviceLen = sizeof(serviceAddr.sun_family) + strlen(SOCKET_SERVICE_ADDRESS) + 1;
serviceLen = sizeof(serviceAddr.sun_family) + sizeof(serviceAddr.sun_path);
printf("绑定的地址为:%s \n", serviceAddr.sun_path);
// 绑定
int bindResult = bind(serviceFd, (struct sockaddr *)&serviceAddr, serviceLen);
if (bindResult == -1)
{
perror("绑定 socket 失败");
exit(0);
}
int listenResult = listen(serviceFd, SOCKET_NUMBER);
if (listenResult == -1)
{
perror("监听 socket 失败");
exit(0);
}
// 创建 epoll, 参数size 实际没作用,但是要大于0
int epollFd = epoll_create(EPOLL_SZIE);
if (epollFd == -1)
{
perror("创建 epoll 失败");
exit(0);
}
// 将对应的文件夹 添加 epoll中
epoll_event epollEvent;
/* *** 注意****
epoll_data 是一个联合体结构成员,所有的成员公用一段内存,因此实际上只能保留一个成员
它只会保存最后一个被赋值的成员,所以不要data.fd,data.ptr都赋值
*/
epollEvent.data.fd = serviceFd;
epollEvent.events = EPOLLIN;
// 循环检测 委托内核去处理
// 当内核检测到事件到来时会将事件写到这个结构体数组里
struct epoll_event events[EPOLL_EVENT_MAX_SZIE];
int addResult = epoll_ctl(epollFd, EPOLL_CTL_ADD, serviceFd, &epollEvent);
if (addResult == -1)
{
perror("添加到 epoll 失败");
exit(0);
}
printf("开始接收消息\n");
while (1)
{
// 等待有没消息返回
// tiemOut 为0 马上返回 为-1 代表阻塞
// maxevents:表示每次能处理的最大事件数,告之内核这个events有多大;这个maxevents的值不能大于创建epoll_create()时的size;
int eventNum = epoll_wait(epollFd, events, EPOLL_EVENT_MAX_SZIE, EPOLL_TIEM_OUT);
printf("收到消息数量: %i \n", eventNum);
for (int i = 0; i < eventNum; i++)
{
// 有连接请求到来,走到这里
if (events[i].data.fd == serviceFd)
{
// 客户端信息填充
struct sockaddr_un clientAddr;
socklen_t clientLen = sizeof(clientAddr);
int connectFd = accept(serviceFd, (struct sockaddr *)&clientAddr, &clientLen);
if (connectFd == -1)
{
perror("接收客户端失败");
exit(0);
}
printf("接收到新的客户端\n");
//将用于通信的文件描述符挂到epoll树上
epollEvent.data.fd = connectFd;
epollEvent.events = EPOLLIN;
epoll_ctl(epollFd, EPOLL_CTL_ADD, connectFd, &epollEvent);
} else
{
// 其他的为客户端发送的值
// 通信也有可能是写事件
if (events[i].events & EPOLLOUT)
{
//这里先忽略写事件
continue;
}
char buf[1024]={0};
int count = read(events[i].data.fd, buf, sizeof(buf));
if (count == 0)
{
// 关闭了客户端要关闭
printf("收到客户端关闭");
close(events[i].data.fd);
epoll_ctl(epollFd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
continue;
}
if (count == -1)
{
perror("接收消息异常退出");
exit(0);
}
printf("收到了消息事件:%s \n", buf);
for (int i = 0; i < sizeof(buf); i++)
{
buf[i] = toupper(buf[i]);
}
write(events[i].data.fd, buf, sizeof(buf));
}
}
}
close(serviceFd);
return 0;
}
如果是单纯1对1通讯,可通过 socketPair 快速建立服务端和客户端
创建一个数组存储对应的ID,创建的socket 会放在该数组里面
int socketFds[2];
// 创建socke 第一个作为服务端 第二个作为客户端
int result = socketpair(SOCKET_DOMIAN, SOCKET_TYPE, SOCKET_PROTOCOL, socketFds);
以下代码设置对应的缓冲区
socklen_t len = sizeof(BUFFER_SZIE);
setsockopt(socketFds[0], SOL_SOCKET, SO_SNDBUF, &BUFFER_SZIE, len);
setsockopt(socketFds[0], SOL_SOCKET, SO_RCVBUF, &BUFFER_SZIE, len);
setsockopt(socketFds[1], SOL_SOCKET, SO_SNDBUF, &BUFFER_SZIE, len);
setsockopt(socketFds[1], SOL_SOCKET, SO_RCVBUF, &BUFFER_SZIE, len);
#include
#include
#include
#include
#include
#include
// 协议域
int SOCKET_DOMIAN = AF_UNIX;
// 类型
int SOCKET_TYPE = SOCK_STREAM;
// 协议,常用tcp udp
int SOCKET_PROTOCOL = 0;
// 缓存区大小
int BUFFER_SZIE = 1024;
int main(void) {
int socketFds[2];
// 创建socke 第一个作为服务端 第二个作为客户端
int result = socketpair(SOCKET_DOMIAN, SOCKET_TYPE, SOCKET_PROTOCOL, socketFds);
if (result == -1)
{
perror("创建 socketPair 错误");
exit(0);
}
/**
* 第一个参数socket是套接字描述符。
* 第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。
* option_name指定准备设置的选项,option_name可以有哪些取值,这取决于level,在套接字级别上(SOL_SOCKET)
* 主要设置缓存区大小
*/
socklen_t len = sizeof(BUFFER_SZIE);
setsockopt(socketFds[0], SOL_SOCKET, SO_SNDBUF, &BUFFER_SZIE, len);
setsockopt(socketFds[0], SOL_SOCKET, SO_RCVBUF, &BUFFER_SZIE, len);
setsockopt(socketFds[1], SOL_SOCKET, SO_SNDBUF, &BUFFER_SZIE, len);
setsockopt(socketFds[1], SOL_SOCKET, SO_RCVBUF, &BUFFER_SZIE, len);
int pid = fork();
printf("孵化出来的进程号为 %i \n", pid);
// 在子进程为0
if (!pid)
{
printf("执行子进程\n");
// 为子进程
close(socketFds[0]);
int count = 0;
while (1)
{
// 休眠一秒
sleep(1);
// 发送给服务端端
write(socketFds[1], &count, sizeof(count));
int size = read(socketFds[1], &count, sizeof(count));
printf("收到服务端数据 %i \n", count);
++count;
}
} else {
printf("执行父进程\n");
// 为父进程
close(socketFds[1]);
int count = 0;
while (1)
{ // 读取客户端数据
int size = read(socketFds[0], &count, sizeof(count));
if (read <= 0)
{
perror("客户端异常退出 \n");
exit(0);
}
printf("收到客户端数据 %i \n", count);
++count;
// 发送给客户端
write(socketFds[0], &count, sizeof(count));
}
}
}