学习笔记|socket

本文最后更新于:18 天前

1.网络应用程序设计模式

1.1 C/S 模式

传统的网络应用设计模式,客户机(client)/ 服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。

优点:

  • 协议选用更加灵活,可以自定义。
  • 可以提前缓存大量数据,提高数据传输的效率。

缺点:

  • 对用户主机构成威胁。
  • 开发团队工作量大,客户端服务端联合调试困难。

1.2 B/S 模式

浏览器(browser)/ 服务器(server)模式。只需在一端部署服务器,而另外一端使用每台 PC 都默认配置的浏览器即可完成数据的传输。

优点:

  • 不会安装软件,相对来说更安全。
  • 不需要开发客户端,工作量相对较小。
  • 跨平台。

缺点:

  • 协议选择不灵活,必须完整的支持协议。
  • 不能数据缓存。

2.socket 概念

2.1 socket 是伪文件

在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。(类比有名管道)

  • 为什么要将套接字封装成伪文件?

    为了统一接口,使读写文件和读写套接字操作一致。

  • 套接字和管道的区别

    管道应用于 本地进程间 通信;套接字应用于 网络进程间 通信。

socket 在数据传输中的位置:

2.2 socket 通信原理

  • IP:在网络环境中唯一标识一台主机。

  • port:在主机中唯一标识一个进程。

  • IP + port:在网络环境中唯一标识一个进程。

所以 IP + port 就对应着一个 socket,想要连接的两个进程通过各自的 socket 来标识,这两个 socket 组成的 socket pair 就唯一标识一个连接。

2.3 socket 的内核实现

  • 文件描述符对应着两个内核缓冲区,一个缓冲区接收数据,一个缓冲区发送数据。

  • 为什么使用两个缓冲区?

    参考管道,只能一个进程读,一个进程写,否则写的数据和读的数据互相覆盖。【半双工

    socket:既能读又能写。【全双工

3.预备知识

3.1 网络字节序

数据包在传输的过程中,需要拆包封包,读取 IP + port,本地字节序是小端方式,而网络字节需是大端方式。

重要:因为 TCP/IP 协议规定,网络数据流应采用大端字节序;而 x86 机器都是小端字节序。

所以需要使用字节序转换函数。

h 表示 host,n 表示 network,l 表示 long 32 位长整型,s 表示 short 16位端整型。

1
2
3
4
5
6
#include <arpa/inet.h>

unit32_t htonl(unit32_t hostlong); // IP 本地字节序 -> 网络字节序
unit16_t htons(unit16_t hostshort); // port 本地字节序 -> 网络字节序
unit32_t ntohl(unit32_t netlong); // IP 网络字节序 -> 本地字节序
unit16_t ntohs(unit16_t netshort); // port 本地字节序 -> 网络字节序

PS:如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回

3.2 IP 地址转换

常见的点分十进制 IP 地址是字符串类型,我们需要将 string 类型,先手动转化为 unsigned int 类型,才能使用 htonl 等函数。于是有了更便捷的函数,直接传入点分十进制 IP 地址的字符串作为参数。

htonl:192.168.1.2 ==》 unsigned int ==》 网络字节序

inet_pton:192.168.1.2 ==》 网络字节序

1
2
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst); // 本地字节序 ==》网络字节序
  • int af:用来表示 ipv4 还是 ipv6。

    • ipv4:af = AF_INET
    • ipv6:af = AF_INT6
  • char *src:本地字节序。点分十进制的 IP 地址。

  • void *dst:网络字节序,传出参数。比如 server_addr.sin_addr

1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);  // 网络字节序 ==》本地字节序
  • int af:同上。

  • void *dst:网络字节序。比如 client_addr.sin_addr

  • char *src:本地字节序。传出参数。

  • socklen_t size:src 字符串的大小。

3.3 sockaddr 数据结构

关于 struct sockaddr:

  • struct sockaddr 结构体诞生早于IPv4协议。

  • struct sockaddr 现在已经废弃,定义结构体时,应使用 sockaddr_in 或 sockaddr_in6。(分别表示 IPV4 和 IPV6)

  • 【重要】但是 bind、accept、connect 等函数的参数,规定的还是 struct sockaddr *

    所以需要强制转换,例如:bind( xxx, (struct sockaddr *) &addr );

关于 sockaddr_in:

netinet/in.h 文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct sockaddr_in {
__kernel_sa_family_t sin_family; // Address family 地址结构类型(简称af)
__be16 sin_port; // Port number 端口号
struct in_addr sin_addr; // Internet address IP地址

/* Pad to size of 'struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};

struct in_addr { /* Internet address. */
__be32 s_addr;
};
  • sin_family:填 AF_INET,表示 ipv4。

  • port:端口号

  • sin_addr:IP 地址

    可以设置成 htol(INADDR_ANY),这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,

  • 其他参数:填充空间的字符,pad:填充

4.网络套接字函数

4.1 socket 函数

创建一个 socket,返回 socket 文件描述符。

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • domain:

    • AF_INET:IPV4 地址
    • AF_INET6:IPV6 地址
    • AF_UNIX:本地协议
  • type:通信协议

    • SOCK_STREAM:这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。典型代表协议 – TCP

    • SOCK_DGRAM:这个协议是无连接的、固定长度的传输调用。典型代表协议 – UDP

    • 不常用协议:

      SOCK_SEQPACKET:该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。

      SOCK_RAW socket:类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)

      SOCK_RDM:这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。

  • protocol:表示协议号。

    • protocol = 0,表示 默认协议。
    • SOCK_STREAM 的默认协议是 TCP,SOCK_DGRAM 的默认协议是 UDP。
  • 返回值:成功,返回新创建的 socket 的文件描述符;失败,返回 -1,设置 errno。(errno 可以使用 perror 打印)

4.2 bind 函数

绑定 IP + port 和 socket 文件描述符。使 sockfd 文件描述符监听 addr 所描述的地址和端口号。

1
2
3
4
5
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> // struct sockaddr_in 所在头文件

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:

  • addr:sockaddr 结构体,存的是 IP + port。

  • addrlen:长度,sizeof(addr),addr 可以有多种协议格式,所以需要指定结构体长度。

4.3 listen 函数

用来声明sockfd处于监听状态,并指定 同时链接 的个数上限。【不是指定链接上限数】

例如:最多只能有 400 个客户端和我同时建立链接。也就是说,处于三次握手过程中的链接数最多是 400 个。

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);
  • sockfd:socket 文件描述符。

  • backlog:处于三次握手过程中的链接数最。

  • 返回值:成功返回 0,失败返回 -1。

查看系统默认 backlog:

1
cat /proc/sys/net/ipv4/tcp_max_syn_backlog

4.4 accept 函数

三方握手完成后,服务器 调用 accept() 接受连接。

如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:socket 文件描述符。

  • addr:传出参数(value-result argumen),返回链接客户端的地址信息(IP + port)

  • addrlen:传入传出参数。【注意是指针 socklen_t*,bind 和 connect 函数的参数不是指针 socklen_t】

    • 传入:addr 还没发生改变时,传入 sizeof(addr),表示传出参数容器的大小。
    • 传出:函数执行后,addr 里面有了真正的数据,传出 sizeof(addr),表示真正接收到的 addr 的大小。
  • 返回值:成功,返回一个的 socket 文件描述符,用于和客户端通信。失败返回 -1,设置 errno。

4.5 connect 函数

客户端 调用 connect() 连接服务器。

connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *addr, socklen_t addrlen);
  • sockfd:socke 文件描述符。

  • addr:传入参数,指定服务器端的信息,IP + port。

  • addrlen:传入参数,sizeof(addr)。

  • 返回值:成功返回 0,失败返回 -1,设置 neero。

5.socket 实例

5.1 CS模型的流程

客户端发送小写字母;服务端收到后,转换为大写字母,并回传给客户端。

5.2 代码实现

server.cc

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
#define SERVER_PORT 6666
int main() {
// std::cout << "1.create..." << std::endl;
int lfd = socket(AF_INET, SOCK_STREAM, 0);

sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

// std::cout << "2.bind..." << std::endl;
bind(lfd, (sockaddr *)&server_addr, sizeof(server_addr));
listen(lfd, 32); // 默认 128

// std::cout << "3.accept..." << std::endl;
sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
bzero(&client_addr, len);

int cfd = accept(lfd, (sockaddr *)&client_addr, &len);
if (cfd == -1) {
std::cout << "fail" << std::endl;
}


// std::cout << "4.put info..." << std::endl;
char client_buf[BUFSIZ];
inet_ntop(AF_INET, &client_addr.sin_addr, client_buf, sizeof(client_buf));
int client_port = ntohs(client_addr.sin_port);
std::cout << "Client IP: " << client_buf << "Client port: " << client_port << std::endl;

// std::cout << "5.change..." << std::endl;
while (1) {
char buf[BUFSIZ];
int n = read(cfd, buf, sizeof(buf));

for (int i = 0; i < n; i++) {
buf[i] = toupper(buf[i]); // 转化为大写
}
write(cfd, buf, n);
}

// std::cout << "6.end..." << std::endl;
close(lfd);
return 0;
}

client.cc

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
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 6666

int main() {
// std::cout << "1.create..." << std::endl;
int cfd = socket(AF_INET, SOCK_STREAM, 0);

sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));

server_addr.sin_family = AF_INET;
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
server_addr.sin_port = htons(SERVER_PORT);

// std::cout << "2.connect..." << std::endl;
int ret = connect(cfd, (sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
std::cout << "fail" << std::endl;
}

// std::cout << "3.change..." << std::endl;
while (1) {
char buf[BUFSIZ];
fgets(buf, sizeof(buf), stdin);
std::cout << "buf = " << buf << std::endl;

write(cfd, buf, strlen(buf));

int n = read(cfd, buf, sizeof(buf));
std::cout << "Response from server: " << std::endl;
write(STDOUT_FILENO, buf, n);
std::cout << std::endl;
}

// std::cout << "4.end..." << std::endl;
close(cfd);
return 0;
}

5.3 遇到的问题

  • inet_pton 函数传出参数不是 &server_addr.sin_addr.s_addr,而是 &server_addr.sin_addr

  • server 的 IP 一定不能错。

  • 如果不使用 toupper 函数,自己实现的话,注意输入非法字符容易报错(中文、数字、大写字母)

  • 重要问题】先关闭 server 后,四次挥手,server 进入 TIME_WAIT 状态,此时端口被占用,无法再次 connect。

    检查端口 6666 是否占用。

    1
    netstat -apn | grep 6666

5.4 错误处理函数

  • 什么是错误处理函数?

    就是对 socket、bind、accept、connec 等函数进行二次封装,将返回值判断封装其中。

  • 为什么要写错误处理函数?

    对于 socket、bind、accept、connect 都应该判断返回值。但是为了保证主程序的逻辑清晰,将判断返回值的步骤放在错误处理函数中。

  • 为什么封装后的函数名要大写?

    socket 封装成 Socket,这样在 vim 中按 shift + k 依然可以打开 man 文档,因为打开 man 文档不区分函数名大小写。

wrap.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "include.hpp"

#ifndef __WRAP_H_
#define __WRAP_H_
void perror_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
void Bind(int fd, const struct sockaddr *sa, socklen_t salen);
void Connect(int fd, const struct sockaddr *sa, socklen_t salen);
void Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
void Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
static ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif

wrap.cc

【注意】if ( (n = accept(fd, sa, salenptr)) < 0) { ,一定要将 n = accept 用括号括起来,因为 < 的优先级高。

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#include "wrap.hpp"

void perror_exit(const char *s) {
perror(s);
exit(-1);
}

int Accept(int fd, sockaddr *sa, socklen_t *salenptr) {

// 调用成功,需要返回一个新的文件描述符,用于与客户端通信。
int n;

again:
if ( (n = accept(fd, sa, salenptr)) < 0) {

// ECONNABORTED:异常断开
// EINTR:慢系统调用被信号中断
if ((errno == ECONNABORTED) || (errno == EINTR)) {
goto again; // 要么重启,要么执行信号的默认动作。这里选择重启。
} else {
perror_exit("accept error");
}
}
return n;
}

void Bind(int fd, const sockaddr *sa, socklen_t salen) {
if (bind(fd, sa, salen) < 0) {
perror_exit("bind error");
}
}

void Connect(int fd, const sockaddr *sa, socklen_t salen) {
if (connect(fd, sa, salen) < 0) {
perror_exit("connect error");
}
}

void Listen(int fd, int backlog) {
if (listen(fd, backlog) < 0) {
perror_exit("listen error");
}
}

int Socket(int family, int type, int protocol) {

// 因为 socket 需要返回文件描述符,所以封装的 Socket 函数需要有返回值。
int n;
if ( (n = socket(family, type, protocol)) < 0) {
perror_exit("socket error");
}
return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes) {
ssize_t n;

again:
4if ( (n = read(fd, ptr, nbytes)) == -1) {
44if (errno == EINTR) {
444goto again;
} else {
444return -1;
}
4}
4return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes) {
ssize_t n;

again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR) {
goto again;
} else {
return -1;
}
}
return n;
}

void Close(int fd) {
if (close(fd) == -1) {
44perror_exit("close error");
}
}

ssize_t Readn(int fd, void *vptr, size_t n) {
size_t nleft; // 剩余未读取的字节数
4ssize_t nread; // 实际读到的值
4char *ptr;

4ptr = (char *)vptr; // 读写指针的位置
4nleft = n;

4while (nleft > 0) {
44if ( (nread = read(fd, ptr, nleft)) < 0) {
444if (errno == EINTR)
4444nread = 0;
444else
4444return -1;
44} else if (nread == 0) // 读完了
444break;

44nleft -= nread;
44ptr += nread;
4}
4return n - nleft;
}


ssize_t Writen(int fd, const void *vptr, size_t n) {

size_t nleft;
4ssize_t nwritten;
4const char *ptr;

4ptr = (char *)vptr;
4nleft = n;
4while (nleft > 0) {
44if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
444if (nwritten < 0 && errno == EINTR)
4444nwritten = 0;
444else
4444return -1;
44}

44nleft -= nwritten;
44ptr += nwritten;
4}
4return n;
}

// Readline 的辅助函数
static ssize_t my_read(int fd, char *ptr) {
4static int read_cnt; // 读到的数量
4static char *read_ptr;
4static char read_buf[100]; // 读到的字符串

4if (read_cnt <= 0) {
again:
44if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
444if (errno == EINTR)
4444goto again;
444return -1;
44} else if (read_cnt == 0)
444return 0;
44read_ptr = read_buf;
4}
4read_cnt--;
4*ptr = *read_ptr++;
4return 1;
}

// 每次读一行,替代 fgets
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
4ssize_t n, rc;
4char c, *ptr;

4ptr = (char *)vptr;
4for (n = 1; n < maxlen; n++) {
44if ( (rc = my_read(fd, &c)) == 1) {
444*ptr++ = c;
444if (c == '\n') // 读到 \n 表示读了一行
4444break;
44} else if (rc == 0) {
444*ptr = 0;
444return n - 1;
44} else
444return -1;
4}
4*ptr = 0;
4return n;
}

6.多进程并发服务器

6.1 思路

每一个客户端,都由一个子进程负责通信。

Server–父进程:

  1. 监听连接部分:

    • 创建 socket,将监听端口绑定到 lfd。
    • 等待 Client 连接,并返回用于通信的 cfd。
  2. 管理子进程部分:

    • 一旦有新 Client 连接,则创建子进程,让子进程负责通信。
    • 捕捉信号(子进程死亡 SIGCHLD),回收子进程。

Server–子进程:

  • 与客户端通信。

Client:

  • 创建 socket,连接服务器。
  • 发送请求。

6.2 代码实现

server.cc

要注意的地方:

  • 父进程不负责通信,所以必须关掉 cfd,以防文件描述符不够用。
  • 子进程不负责连接和创建子进程,需要退出循环并关掉 lfd。
  • 子进程通信时,如果客户端关闭写端,此时 read 的返回值 n == 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
68
69
70
71
void wait_child(int signo) {
pid_t wait_pid = 0;
while ((wait_pid = waitpid(0, NULL, WNOHANG)) > 0) {
std::cout << "child_pid = " << wait_pid << " is end!" << std::endl;
}
}

#define SERVER_PORT 8888
int main() {
//1.创建 socket
int lfd = Socket(AF_INET, SOCK_STREAM, 0);

//2.绑定 IP+port
sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// inet_pton(AF_INET, "172.16.103.5", &server_addr.sin_addr);
Bind(lfd, (sockaddr*)&server_addr, sizeof(server_addr));

//3.设置最大同时连接数
Listen(lfd, 128);

int cfd;
pid_t pid;
while (1) {

//4.父进程等待客户端连接
sockaddr_in client_addr;
bzero(&client_addr, sizeof(client_addr));
socklen_t client_addr_len = sizeof(client_addr);
cfd = Accept(lfd, (sockaddr*)&client_addr, &client_addr_len);
char client_buf[BUFSIZ];

// 输出客户端信息
inet_ntop(AF_INET, &client_addr.sin_addr, client_buf, sizeof(client_buf));
int client_port = ntohs(client_addr.sin_port);
std::cout << "Client IP: " << client_buf << "Client port: " << client_port << std::endl;

//5.创建子进程
pid = fork();
if (pid < 0) {
perror_exit("fork error");
} else if (pid == 0) {
close(lfd); // 子进程不负责连接,推出“连接-创建子进程”的循环。
break;
} else {
close (cfd); // 父进程不负责通信,回收子进程
signal(SIGCHLD, wait_child);
}
}

// 6.子进程通信
if (pid == 0) {
while (1) {
char read_buf[BUFSIZ] = {0};
int n = Read(cfd, read_buf, sizeof(read_buf));
if (n == 0) { // client 关闭写端
close(cfd);
return 0;
} else { // 处理请求
for (int i = 0; i < n; i++) {
read_buf[i] = toupper(read_buf[i]);
}
Write(cfd, read_buf, n);
}
}
}
return 0;
}

client.cc

5.2 中的 client 相同。

因为客户端的逻辑并没有改变,还是连接服务器 + 发送请求。

7.多线程并发服务器

7.1 思路

和多线程并发思路一致,只是将进程换成线程。

每一个客户端,都由一个子线程负责通信。

Server–父线程:

  1. 监听连接部分:

    • 创建 socket,将监听端口绑定到 lfd。
    • 等待 Client 连接,并返回用于通信的 cfd。
  2. 管理子线程部分:

    • 一旦有新 Client 连接,则创建子线程,让子线程负责通信。
    • 并设置线程分离。

Server–子线程:

  • 调用回调函数 communicate,与客户端通信。

Client:

  • 创建 socket,连接服务器。
  • 发送请求。

7.2 代码实现

server.cc

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
struct socket_info {   // 构建一个合理的结构体,既能传递 client_addr, 也能传递 cfd
sockaddr_in client_addr;
int cfd;
};

void *communicate(void *arg) { // 线程的回调函数,返回值和参数都是 void *
socket_info *s_info = (socket_info *)arg;

// 输出客户端信息
char client_buf[BUFSIZ] = {0};
inet_ntop(AF_INET, &s_info->client_addr.sin_addr, // -> 的优先级大于 &
client_buf, sizeof(client_buf));
int client_port = ntohs(s_info->client_addr.sin_port);
std::cout << "Client IP: " << client_buf << " Client port: " << client_port << std::endl;

// 子线程通信
while (1) {
char read_buf[BUFSIZ] = {0};
int n = Read(s_info->cfd, read_buf, sizeof(read_buf));
if (n == 0) { // client 关闭了写端
close(s_info->cfd);
break;
} else { // 处理请求
for (int i = 0; i < n; i++) {
read_buf[i] = toupper(read_buf[i]);
}
Write(s_info->cfd, read_buf, n);
}
}
close(s_info->cfd);
return (void *)0;
}


#define SERVER_PORT 8888
int main() {
//1.创建 socket
int lfd = Socket(AF_INET, SOCK_STREAM, 0);

//2.绑定 IP+port
sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// inet_pton(AF_INET, "172.16.103.5", &server_addr.sin_addr);
Bind(lfd, (sockaddr*)&server_addr, sizeof(server_addr));

//3.设置最大同时连接数
Listen(lfd, 128);

int cfd;
pid_t pid;
socket_info s_info[256]; // 用数组存每一个连接的 client_addr + cfd
int i = 0;
pthread_t pth_id;
while (1) {

//4.父线程等待客户端连接
socklen_t client_addr_len = sizeof(s_info[i].client_addr);
bzero(&s_info[i].client_addr, client_addr_len);
s_info[i].cfd = Accept(lfd, (sockaddr*)&s_info[i].client_addr, &client_addr_len);

//5. 创建子线程来处理通信
pthread_create(&pth_id, NULL, communicate, (void *)&s_info[i]); // 子线程调用回调函数 communicate
pthread_detach(pth_id); // 父子线程分离,子线程自动回收自己的 PCB
i++; // 标记第几个线程,以便存入 client_addr + cfd
}
return 0;
}

client.cc

5.2 中的 client 相同。

因为客户端的逻辑并没有改变,还是连接服务器 + 发送请求。

8.socket 与 TCP

8.1 TCP 状态转换

计算机网络笔记

8.2 socket 与 TCP 对应

###8.3 TIME_WAIT 状态

先关闭 server 后,四次挥手,server 进入 TIME_WAIT 状态,此时端口被占用,无法再次 connect。

检查端口 6666 是否占用。

1
netstat -apn | grep 6666

8.4 FIN_WAIT_2 状态

可以使用 API 实现半关闭状态。

1
2
#include <sys/stocket.h>
int shutdown(int sockfd, int how);
  • sockfd:要关闭的文件描述符。
  • how:关闭的方式。
    • SHUT_RD(0):关闭sockfd上的读功能,该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
    • SHUT_WR(1):关闭sockfd的写功能,进程不能在对此套接字发出写操作。
    • SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是 SHUT_RD,然后是SHUT_WR。

PS:shutdown 和 close 的区别

  • close 中止连接,减少描述符的引用计数,并不直接关闭连接,当描述符引用计数为 0 时,才关闭连接。

  • shutdown不考虑描述符的引用计数,比如关闭读端,任何进程都无法从该 socket 接收数据。

举个例子:

  • 多个进程共享一个套接字,close 被调用一次,计数减一,直到所有进程均调用 close,套接字才被释放。(套接字就是内核中的两个缓冲区)
  • 在多进程中,如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程close(sfd)将不会影响到其它进程。

8.5 端口复用

函数原型:

该函数用法极其复杂,记住咱们设置端口复用即可。

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • sockfd:套接字文件描述符
  • level:级别
  • optname:选项名
  • optval:选项值
  • optlen:选项的字节长度

设置端口复用:

在 bind 之前,设置。

1
2
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  • SOL_SOCKET:级别

  • SO_REUSEADDR:允许重用本地端口

9.多路 IO 转接服务器

也叫多任务 IO 服务器。

9.1 思想

不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。

方式有三种:

  • select
  • pselect
  • poll

10.select

10.1 函数原型

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

struct timeval {
long tv_sec; // seconds
long tv_usec; // microseconds
};
  • nfds:所监听的所有文件描述符中,最大的文件描述符 + 1。

    意义:此参数会告诉内核检测前多少个文件描述符的状态。

    比如:进程打开了序号为 0 - 400 的文件描述符,nfds 则为 401。

  • fd_set:位图,用二进制来标记文件描述符是否在集合中。

    • readfds:监听文件描述符集合 readfds 中,是否有可读事件。传入传输参数。

    • writefds:监听文件描述符集合 writefds 中,是否有可写事件。传入传输参数。

    • exceptfds:监听文件描述符集合 exceptfds 中,是否有异常事件。传入传输参数。

    【传入传出】举个例子

    • 调用 select 之前,我们想监听 4、5 文件描述符是否有可读事件。

      readfds 集合中,4、5 文件描述符对应的值为 1,其他的为 0。

    • 调用 select 之后,只有 4 文件描述符有可读事件。

      readfds 集合中,4 文件描述符对应的值为 1,其他的为 0。

  • timeout:内核监听的时间。

    • NULL,永远等下去
    • 设置 timeval,等待固定时间
  • 设置 timeval 里时间均为 0,检查描述符后立即返回,轮询

  • 返回值

    • 成功,返回所监听的所有监听集合中,满足条件的总数。

      举个例子:

      r:3,4。可读事件:3,4

      w:4,5,6。可写事件:4,5

      e:4,5,7,8。异常事件:4,7

      select 返回 6。

    • 失败,则返回 -1,设置 errno。

10.2 设置 fd_set

类似设置未决信号集、阻塞信号集(类型 sigset_t)

【全部清除】将 set 所有位置 0。

1
void FD_ZERO(fd_set *set);

【清除 fd】将 set 中对应的 fd 置 0。

1
void FD_CLR(int fd, fd_set *set);

【添加 fd】将 set 中对应的 fd 置 1。

1
void FD_SET(int fd, fd_set *set);

【判断 fd 是否在 set 中】判读 set 中对应的 fd 是否为 1。

1
int FD_ISSET(int fd, fd_set *set);
  • 返回值:在 set 中返回 1,不在 set 中返回 0。

10.3 select 的局限性

  • 文件描述符上限:1024。

    也就是说同时监听文件描述符 1024 个。

  • select 返回值只返回总数,如果需要知道具体的哪一个文件描述符,需要 for 循环判断 0 - 1023 个文件描述符。

    解决方案:自定义结构体(数组),将要监听的文件描述符加入数组。for 循环时,只循环判断数组中的文件描述符。

  • 监听集合、满足监听条件的集合是同一个集合。

    所以需要保存原有的监听集合,否则返回后监听集合就被破坏了。


本博客所有文章均个人原创,除特别声明外均采用 CC BY-SA 4.0协议,转载请注明出处!