Linux网络编程

一些Linux网络编程的操作和姿势


Linux网络编程

字节序

字节序:字节在内存中存储的顺序

小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的地位地址

大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的地位地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 通过代码检测当前主机的字节序

#include <stdio.h>
int main(){

union {
short value; // 2 字节
char bytes[sizeof(short)]; // char[2]
}test;

test.value = 0x0102;
if(test.bytes[0] == 1 && (test.bytes[1] == 2)){
printf("大端序\n");
}else if(test.bytes[0] == 2 && (test.bytes[1] == 1)){
printf("小端序\n");
}else{
printf("未知\n");
}

return 0;
}

网络字节顺序是TCPIP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式

1
2
3
4
5
6
7
8
#include <arpa/inet.h>
// 转端口
uint16_t htons(uint16_t hostshort); // 主机->网络
uint16_t ntohs(uint16_t netshort); // 网络->主机

// 转IP
uint32_t htol(uint32_t hostlong); // 主机->网络
uint32_t ntohl(uint32_t netlong); // 网络->主机
示例程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <arpa/inet.h>
#include <stdio.h>
int main(){

// htons 转端口
unsigned short a = 0x0102;
printf("%04x\n", a);
unsigned short b = htons(a);
printf("%04x\n", b);

printf("=================\n");

// htonl 转IP
char buf[4] = {192, 168, 1, 100};
int num = *(int *)buf;
int sum = htonl(num);
unsigned char *p = (char *)&sum;
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));

printf("=================\n");

// ntohl
unsigned char buf1[4] = {1, 1, 168, 192};
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char *p1 = (unsigned char *)&sum1;
printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));

return 0;
}

socket地址

socket地址是一个结构体,封装端口号和IP等信息,后面的socket相关的API中需要使用到这个socket地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <bits/socket.h>
struct sockaddr{
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址值(用于IPv4)
};
typedef unsigned short int sa_family_t; // 地址族类型
// 以上都太麻烦,应用时使用专用socket地址

struct sockaddr_storage{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;

专用socket地址

通常使用中采用如下专用socket地址格式,其中struct sockaddr_in最为常用。

专用socket地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof(in_port_t) - sizeof(struct in_addr)];
};
struct in_addr {
in_addr_t s_addr;
};
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof(unsigned short int))

所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。


IP地址转换(字符串ip-整数,主机、网络字节序转换)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af: 地址族: AF_INET AF_INET6
src: 需要转换的点分十进制的IP字符串
dst: 传出参数,转换后的结果保存在这个里面

// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

套接字函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命

- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小

int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1

ssize_t write(int fd, const void *buf, size_t count); // 写数据

ssize_t read(int fd, void *buf, size_t count); // 读数据

TCP通信并发

要实现TCP通信服务器并发的任务,使用多线程/多进程来解决。

思路:

  • 一个父进程,多个子进程
  • 父进程负责等待并接收客户端连接
  • 子进程:完成通信,接受一个客户端连接,就创建一个子进程用于通信

详细代码参考我的github:linux_learn/socket at main · b4158813/linux_learn (github.com)


半关闭 & 端口复用

半关闭

当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发 送的数据,但是 A 已经不能再向 B 发送数据。

从程序的角度,可以使用 API 来控制实现半连接状态:

1
2
3
4
5
6
7
8
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。

使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用 计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方 向的连接,只中止读或只中止写。

注意:

  1. 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用 进程都调用了 close,套接字将被释放。
  2. 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。 但如果一个进程 close(sfd) 将不会影响到其它进程。

端口复用

端口复用最常用的用途是:

  • 防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出而系统没有释放端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/types.h>
#include <sys/socket.h>
// 设置套接字属性(不仅仅设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数:
- sockfd : 要操作的文件描述符
- level : 级别,SOL_SOCKET(端口复用的级别)
- optname : 选项的名称
- SO_REUSEADDR
- SO_REUSEPORT
- optval : 端口复用的值(整形)
- 1 : 可以复用
- 0 : 不可以复用
- optlen : optval参数的大小

端口复用,设置的时机是在服务器绑定端口之前。
setsockopt()
bind()

查看网络相关信息的命令:

1
2
3
4
5
netstat
参数:
-a 所有的socket
-p 显示正在使用socket的程序的名称
-n 直接使用IP地址,而不通过域名服务器

I/O多路复用(多路转接)

I/O多路复用能使得程序能同时监听多个文件描述符,能够提高程序性能,Linux下实现I/O多路复用的系统调用主要有 select, poll, epoll

Blocking IO模型

NIO 模型


select

主旨思想:

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。

  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O 操作时,该函数才返回。

    a. 这个函数是阻塞

    b. 函数对文件描述符的检测的操作是由内核完成的

  1. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// sizeof(fd_set) = 128 1024
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数:
- nfds : 委托内核检测的最大文件描述符的值 + 1
- readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
- 一般检测读操作
- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
- 是一个传入传出参数
- writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
- 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
- exceptfds : 检测发生异常的文件描述符的集合
- timeout : 设置的超时时间
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- NULL : 永久阻塞,直到检测到了文件描述符有变化
- tv_sec = 0 tv_usec = 0, 不阻塞
- tv_sec > 0 tv_usec > 0, 阻塞对应的时间
- 返回值 :
- -1 : 失败
- >0(n) : 检测的集合中有n个文件描述符发生了变化

// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);

// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);

// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);

// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);

select缺点:

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 (O(n)遍历)
  3. select支持的文件描述符数量太小了,默认是1024
  4. fds集合不能重用,每次都需要重置

select 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>

int main(){

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);

// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

// 监听
listen(lfd, 8);

// 创建一个fd_set的集合,存放的是需要检测的文件描述符
fd_set rdset, tmp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
printf("maxfd = %d\n", maxfd);

while(1){

tmp = rdset;

// 调用select系统函数,让内核帮检测哪些文件描述符有数据
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if(ret == -1){
perror("select");
exit(-1);
}else if(ret == 0){
continue;
}else if(ret > 0){
// 说明检测到了有文件描述符对应的缓冲区的数据发生了改变
if(FD_ISSET(lfd, &tmp)){
// 表示有新的客户端连接进来
struct sockaddr_in caddr;
int len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len);

// 将新的文件描述符加入到集合中
FD_SET(cfd, &rdset);

// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}

for(int i = lfd + 1; i <= maxfd; ++ i){
if(FD_ISSET(i, &tmp)){
// 说明这个文件描述符对应客户端发送了数据
char buf[1024] = {0};
int len = read(i, buf, sizeof buf);
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
close(i);
FD_CLR(i, &rdset);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}

close(lfd);



return 0;
}

poll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <poll.h>
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 */
};
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数:
- fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
- nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
- timeout : 阻塞时长
- 0 : 不阻塞
- 1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
- > 0 : 阻塞的时长
- 返回值:
-1 : 失败
>0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化

poll宏值


poll 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <poll.h>

int main(){

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);

// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

// 监听
listen(lfd, 8);

// 初始化检测的文件描述符数组
struct pollfd fds[1024];
for(int i = 0; i < 1024; ++ i){
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int nfds = 0;

while(1){

// 调用poll系统函数,让内核帮检测哪些文件描述符有数据
int ret = poll(fds, nfds + 1, -1);
if(ret == -1){
perror("select");
exit(-1);
}else if(ret == 0){
continue;
}else if(ret > 0){
// 说明检测到了有文件描述符对应的缓冲区的数据发生了改变
if(fds[0].revents & POLLIN){
// 表示有新的客户端连接进来
struct sockaddr_in caddr;
int len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len);
char cip[16];
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, cip, sizeof cip);
unsigned short cport = ntohs(caddr.sin_port);
printf("new request accepted, IP = %s, port = %d\n", cip, cport);
// 将新的文件描述符加入到集合中
for(int i = 1; i < 1024; ++ i){
if(fds[i].fd == -1){
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}

// 更新最大的文件描述符
nfds = nfds > cfd ? nfds : cfd;
}

for(int i = 1; i <= nfds; ++ i){
if(fds[i].revents & POLLIN){
// 说明这个文件描述符对应客户端发送了数据
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof buf);
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
close(fds[i].fd);
fds[i].fd = -1;
}else if(len > 0){
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}

close(lfd);

return 0;
}

epoll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <sys/epoll.h>

// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。
int epoll_create(int size);
- 参数:
size : 目前没有意义了。随便写一个数,必须大于0
- 返回值:
-1 : 失败
> 0 : 文件描述符,操作epoll实例的

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

常见的Epoll检测事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
...

// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数:
- epfd : epoll实例对应的文件描述符
- op : 要进行什么操作
EPOLL_CTL_ADD: 添加
EPOLL_CTL_MOD: 修改
EPOLL_CTL_DEL: 删除
- fd : 要检测的文件描述符
- event : 检测文件描述符什么事情

// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数:
- epfd : epoll实例对应的文件描述符
- events : 传出参数,保存了发送了变化的文件描述符的信息
- maxevents : 第二个参数结构体数组的大小
- timeout : 阻塞时间
- 0 : 不阻塞
- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
- > 0 : 阻塞的时长(毫秒)
- 返回值:
- 成功,返回发生变化的文件描述符的个数 > 0
- 失败 -1

epoll 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(){

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);

// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

// 监听
listen(lfd, 8);

// 用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);

// 将监听的文件描述符相关检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

struct epoll_event epevs[1024];

while(1){

int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1){
perror("epoll_wait");
exit(-1);
}

printf("res = %d\n", ret);

for(int i = 0; i < ret; ++ i){
int curfd = epevs[i].data.fd;
if(curfd == lfd){
// 监听文件描述符有客户端连接
struct sockaddr_in caddr;
int len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len);

epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}else{
// 最好对每一种事件做特定处理
if(epevs[i].events & EPOLLOUT){
continue;
}
// 有数据到达,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof buf);
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}

close(lfd);
close(epfd);


return 0;
}

epoll的工作模式

  • LT模式(水平触发)

    假设委托内核检测读事件 => 检测到fd的读缓冲区

    ​ 读缓冲区有数据 => epoll检测到了会给用户通知

    ​ a. 用户不读数据,数据一直在缓冲区,epoll会一直通知

    ​ b. 用户只读了一部分数据,epoll也会继续通知

    ​ c. 缓冲区的数据读完了,epoll不通知

LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这 种做中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。

  • ET模式(边沿触发)

    假设委托内核检测读事件 => 检测到fd的读缓冲区

    ​ 读缓冲区有数据 => epoll检测到了会给用户通知

    ​ a. 用户不读数据,数据一直在缓冲区中,epoll下次检测时不通知

    ​ b. 用户读了一部分数据,epoll下一次也不会通知

    ​ c. 缓冲区的数据读完了,epoll不通知

ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述 符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪, 并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述 符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成 未就绪),内核不会发送更多的通知(only once)。

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写 操作把处理多个文件描述符的任务饿死。

1
2
3
4
5
6
7
8
9
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
- EPOLLET

LT模式 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(){

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);

// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

// 监听
listen(lfd, 8);

// 用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);

// 将监听的文件描述符相关检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

struct epoll_event epevs[1024];

while(1){

int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1){
perror("epoll_wait");
exit(-1);
}

printf("res = %d\n", ret);

for(int i = 0; i < ret; ++ i){
int curfd = epevs[i].data.fd;
if(curfd == lfd){
// 监听文件描述符有客户端连接
struct sockaddr_in caddr;
int len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len);

epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}else{
// 最好对每一种事件做特定处理
if(epevs[i].events & EPOLLOUT){
continue;
}
// 有数据到达,需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof buf);
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}

close(lfd);
close(epfd);


return 0;
}

ET模式 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main(){

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);

// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

// 监听
listen(lfd, 8);

// 用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);

// 将监听的文件描述符相关检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

struct epoll_event epevs[1024];

while(1){

int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1){
perror("epoll_wait");
exit(-1);
}

printf("res = %d\n", ret);

for(int i = 0; i < ret; ++ i){
int curfd = epevs[i].data.fd;
if(curfd == lfd){
// 监听文件描述符有客户端连接
struct sockaddr_in caddr;
int len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len);

// 设置cfd属性非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);

epev.events = EPOLLIN | EPOLLET; // 设置边沿触发
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}else{
// 最好对每一种事件做特定处理
if(epevs[i].events & EPOLLOUT){
continue;
}

/*
ET模式不可以使用如下方法

// 有数据到达,需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof buf);
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
*/

// 一次性读取出所有数据(循环读取)
char buf[5];
int len = 0;
while((len = read(curfd, buf, sizeof(buf))) > 0){
// 打印数据
printf("recv data : %s\n", buf);
write(curfd, buf, len);
}

if(len == 0){
printf("client closed...\n");
}else if(len == -1){
if(errno == EAGAIN){
printf("data over...\n");
}else{
perror("read");
exit(-1);
}
}
}
}
}

close(lfd);
close(epfd);


return 0;
}

UDP通信实现

UDP通信流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
- 参数:
- sockfd : 通信的fd
- buf : 要发送的数据
- len : 发送数据的长度
- flags : 0
- dest_addr : 通信的另外一端的地址信息
- addrlen : 地址的内存大小

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
- 参数:
- sockfd : 通信的fd
- buf : 接收数据的数组
- len : 数组的大小
- flags : 0
- src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL
- addrlen : 地址的内存大小

UDP 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main(){

// 创建udp通信socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}

// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr *)&saddr, sizeof saddr);
if(ret == -1){
perror("bind");
exit(-1);
}

// 通信
while(1){

char recvbuf[128];
struct sockaddr_in caddr;
int len = sizeof caddr;
// 接收数据
int num = recvfrom(fd, recvbuf, sizeof recvbuf, 0, (struct sockaddr *)&caddr, &len);

char ipbuf[16];
printf("client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ipbuf, sizeof ipbuf),
ntohs(caddr.sin_port)
);

printf("client say: %s\n", recvbuf);

// 发送数据
sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&caddr, sizeof caddr);

}

close(fd);

return 0;
}

UDP 客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main(){

// 创建udp通信socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "172.18.0.1", &saddr.sin_addr.s_addr);

// 通信
int num = 0;
while(1){

// 发送数据
char sendbuf[128];
sprintf(sendbuf, "hello, I am client %d\n", num++);
sendto(fd, sendbuf, strlen(sendbuf) + 1, 0, (struct sockaddr *)&saddr, sizeof saddr);

// 接收数据
int num = recvfrom(fd, sendbuf, sizeof sendbuf, 0, NULL, NULL);

printf("server say: %s\n", sendbuf);

sleep(1);
}

close(fd);

return 0;
}

广播

向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广 播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。

a.只能在局域网中使用。

b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。

1
2
3
4
5
6
7
8
// 设置广播属性的函数
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t
optlen);
- sockfd : 文件描述符
- level : SOL_SOCKET
- optname : SO_BROADCAST
- optval : int类型的值,为1表示允许广播
- optlen : optval的大小

广播 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main(){

// 创建udp通信socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}


// 设置广播属性
int op = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof op);
// 创建一个广播地址
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "172.18.255.255", &cliaddr.sin_addr.s_addr);

// 通信
int num = 0;
while(1){

char sendbuf[128];
sprintf(sendbuf, "hello, client... %d\n", num++);
// 发送数据
sendto(fd, sendbuf, strlen(sendbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof cliaddr);
printf("广播的数据:%s\n", sendbuf);
sleep(1);

}

close(fd);

return 0;
}

广播 客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main(){

// 创建udp通信socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}

// 客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr *)&addr, sizeof addr);
if(ret == -1){
perror("bind");
exit(-1);
}

// 通信
while(1){

char buf[128];
// 接收数据
int num = recvfrom(fd, buf, sizeof buf, 0, NULL, NULL);

printf("server say: %s\n", buf);

}

close(fd);

return 0;
}

组播(多播)

单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。 单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方 案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上 的接口接收。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。

a.组播既可以用于局域网,也可以用于广域网

b.客户端需要加入多播组,才能接收到多播的数据

多播 示意图

组播地址

IP 多播通信必须依赖于 IP 多播地址,在 IPv4 中它的范围从 224.0.0.0 到 239.255.255.255 , 并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三类

多播地址


设置组播:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
// 服务器设置多播的信息,外出接口
2. 本地套接字
本地套接字的作用:本地的进程间通信
有关系的进程间的通信
没有关系的进程间的通信
本地套接字实现流程和网络套接字类似,一般呢采用TCP的通信流程。
- level : IPPROTO_IP
- optname : IP_MULTICAST_IF
- optval : struct in_addr
// 客户端加入到多播组:
- level : IPPROTO_IP
- optname : IP_ADD_MEMBERSHIP
- optval : struct ip_mreq

struct ip_mreq
{
/* IP multicast address of group. */
struct in_addr imr_multiaddr; // 组播的IP地址
/* Local IP address of interface. */
struct in_addr imr_interface; // 本地的IP地址
};

typedef uint32_t in_addr_t;
struct in_addr{
in_addr_t s_addr;
};


多播 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main(){

// 创建udp通信socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}


// 设置多播的属性,设置外出接口
struct in_addr imr_multiaddr;
// 初始化多播地址
inet_pton(fd, "239.0.0.10", &imr_multiaddr.s_addr);
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof imr_multiaddr);

// 初始化客户端的地址信息
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "239.0.0.10", &cliaddr.sin_addr.s_addr);

// 通信
int num = 0;
while(1){

char sendbuf[128];
sprintf(sendbuf, "hello, client... %d\n", num++);
// 发送数据
sendto(fd, sendbuf, strlen(sendbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof cliaddr);
printf("多播的数据:%s\n", sendbuf);
sleep(1);

}

close(fd);

return 0;
}

多播 客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main(){

// 创建udp通信socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}

// 客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr *)&addr, sizeof addr);
if(ret == -1){
perror("bind");
exit(-1);
}

struct ip_mreq op;
inet_pton(AF_INET, "239.0.0.10", &op.imr_multiaddr.s_addr);
op.imr_interface.s_addr = INADDR_ANY;
// 加入到多播组
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof op);

// 通信
while(1){

char buf[128];
// 接收数据
int num = recvfrom(fd, buf, sizeof buf, 0, NULL, NULL);

printf("server say: %s\n", buf);

}

close(fd);

return 0;
}

本地套接字

本地套接字的作用:本地的进程间通信

​ 有关系的进程间的通信

​ 没有关系的进程间的通信

本地套接字实现流程和网络套接字类似,一般采用TCP的通信流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 本地套接字通信的流程 - tcp

// 服务器端
1. 创建监听的套接字
int lfd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);

2. 监听的套接字绑定本地的套接字文件 -> server端
struct sockaddr_un addr;
// 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
bind(lfd, addr, len);

3. 监听
listen(lfd, 100);

4. 等待并接受连接请求
struct sockaddr_un cliaddr;
int cfd = accept(lfd, &cliaddr, len);

5. 通信
接收数据:read/recv
发送数据:write/send

6. 关闭连接
close();


// 客户端
1. 创建通信的套接字
int fd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);

2. 监听的套接字绑定本地的IP 端口
struct sockaddr_un addr;
// 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
bind(lfd, addr, len);

3. 连接服务器
struct sockaddr_un serveraddr;
connect(fd, &serveraddr, sizeof(serveraddr));

4. 通信
接收数据:read/recv
发送数据:write/send

5. 关闭连接
close();

本地套接字通信 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main(){

// 每次运行前清空套接字文件
unlink("server.sock");

// 创建监听套接字
int lfd = socket(PF_LOCAL, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}

// 绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "server.sock");
int ret = bind(lfd, (struct sockaddr *)&addr, sizeof addr);
if(ret == -1){
perror("bind");
exit(-1);
}

// 监听
ret = listen(lfd, 100);
if(ret == -1){
perror("listen");
exit(-1);
}

// 等待客户端连接
struct sockaddr_un caddr;
int len = sizeof caddr;
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len);
if(cfd == -1){
perror("accept");
exit(-1);
}

printf("client socket filename: %s\n", caddr.sun_path);

// 通信
while(1){

char buf[128];
int len = recv(cfd, buf, sizeof buf, 0);
if(len == -1){
perror("recv");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
break;
}else if(len > 0){
printf("client say: %s\n", buf);
send(cfd, buf, len, 0);
}

}

close(cfd);
close(lfd);

return 0;
}

本地套接字通信 客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main(){

// 每次运行前清空套接字文件
unlink("client.sock");

// 1. 创建套接字
int cfd = socket(PF_LOCAL, SOCK_STREAM, 0);
if(cfd == -1){
perror("socket");
exit(-1);
}

// 2. 绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "client.sock");
int ret = bind(cfd, (struct sockaddr *)&addr, sizeof addr);
if(ret == -1){
perror("bind");
exit(-1);
}

// 3. 连接服务器
struct sockaddr_un saddr;
saddr.sun_family = AF_LOCAL;
strcpy(saddr.sun_path, "server.sock");
ret = connect(cfd, (struct sockaddr *)&saddr, sizeof saddr);
if(ret == -1){
perror("connect");
exit(-1);
}

printf("server socket filename: %s\n", saddr.sun_path);

// 通信
int num = 0;
while(1){

char buf[128];
sprintf(buf, "hello, I am client %d\n", num++);
send(cfd, buf, strlen(buf) + 1, 0);
printf("client say: %s\n", buf);
int len = recv(cfd, buf, sizeof buf, 0);
if(len == -1){
perror("send");
exit(-1);
}else if(len == 0){
printf("server closed...\n");
break;
}else if(len > 0){
printf("server say: %s\n", buf);
}

sleep(1);
}

close(cfd);

return 0;
}