C++ advance(六)Linux 高并发网络编程开发 --2.tcp 三次握手 - 并发

回顾:socket tcp server 实现

1. 创建套接字

1
int lfd = socket ();

2. 绑定本地 IP 和端口

1
2
3
4
struct sockaddr_in serv;
serv.port = htons (port);
serv.IP = htonl (INADDR_ANY);
bind (lfd. &serv, sizeof (serv));

3. 监听

1
listen (lfd, 128);

4. 等待并接收连接请求

1
2
3
4
5
struct sockaddr_in client;
int len = sizeof (client);
int cfd = accept (lfd, &client, &len);
//lfd 监听有没有人连接到 Server,如果有就执行 accept
//cfd 是连接之后,用于通信的

5. 通信

接收数据:read/recv
发送数据:write/send

6. 关闭

close (lfd);
close (cfd);

回顾:socket tcp client 实现

1. 创建套接字

1
int fd = socket;

2. 连接服务器

1
2
3
4
5
6
// 客户端的端口可以不用固定,直接占用一个空闲端口就可以了(随机分配)
struct sockaddr_in server;
server.port = ...;
server.ip = ...;
server.family = ...;
connect (fd, &server, sizeof (server));

3. 通信

接收数据:read/recv
发送数据:write/send

4. 断开连接

1
close (fd);

一、TCP 客户端编程

1.socket () 函数

1
int socket (int domain, int type, int protocol);
(1) domain:即协议域,又称为协议族(family), 地址族。

常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称 AF_UNIX,Unix 域 socket)、AF_ROUTE 等等。协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4 地址(32 位的)与端口号(16 位的)的组合、AF_UNIX 决定了要用一个绝对路径名作为地址。

(2) type:指定 socket 类型。

常用的 socket 类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET 等等(socket 的类型有哪些?)。这个参数指定一个套接口的类型,套接口可能的类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW 等等,它们分别表明字节流、数据报、有序分组、原始套接口。这实际上是指定内核为我们提供的服务抽象,比如我们要一个字节流。需要注意的,并不是每一种协议簇都支持这里的所有的类型,所以类型与协议簇要匹配。

(3) protocol:故名思意,就是指定协议。

常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC 等,它们分别对应 TCP 传输协议、UDP 传输协议、STCP 传输协议、TIPC 传输协议。详见 usr/include/linux/in.h。
当 protocol 为 0 时,会自动选择 type 类型对应的默认协议。

(4) 返回值

当我们调用 socket 创建一个 socket 时,返回的 socket 描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用 bind () 函数,否则就当调用 connect ()、listen () 时系统会自动随机分配一个端口。

2.memset () 函数

string.h 的函数 memset () 将某一内存块的前 n 个字元全部设定为某一资字元。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <string.h>

int main (void)
{
char s [];
printf ("% s\n", memset (s, 'n', 13));
return 0;
}

3.htons () 函数

4.inet_pton () 函数

5.bind () 函数

1
int bind (int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

当一个套接字被创建后,存在一个名字空间(地址族),但它没有被命名。bind () 将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以对 socket 定位。

bind () 用来设置给参数 sockfd 的 socket 一个名称。此名称由参数 my_addr 指向一 sockaddr 结构,对于不同的 socket domain 定义了一个通用的数据结构。

1
2
3
4
struct sockaddr {
unsigned short int sa_family;
char sa_data [14];
};
sa_family 为调用 socket () 时的 domain 参数,即 AF_xxxx 值。
sa_data 最多使用 14 个字符长度。

此 sockaddr 结构会因使用不同的 socket domain 而有不同结构定义,例如使用 AF_INET domain,其 socketaddr 结构定义便为:

1
2
3
4
5
6
struct socketaddr_in {
unsigned short int sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero [8];
};
1
2
3
struct in_addr {
uint32_t s_addr;
};
sin_family 即为 sa_family
sin_port 为使用的 port 编号
sin_addr.s_addr 为 IP 地址
sin_zero 未使用,是为了让 sockaddr 与 sockaddr_in 两个数据结构保持大小相同而保留的空字节。

6.connect () 函数
1
int  connect (int  sockfd,  const struct sockaddr *serv_addr, socklen_t addrlen);

connect () 用来将参数 sockfd 的 socket 连至参数 serv_addr 指定的网络地址。参数 addrlen 为 sockaddr 的结构长度。
返回值:成功则返回 0,失败返回 - 1,错误原因存于 errno 中。

7.fgets () 函数

虽然用 gets () 时有空格也可以直接输入,但是 gets () 有一个非常大的缺陷,即它不检查预留存储区是否能够容纳实际输入的数据,换句话说,如果输入的字符数目大于数组的长度,gets 无法检测到这个问题,就会发生内存越界,所以编程时建议使用 fgets ()。
fgets () 的原型为:

1
2
# include <stdio.h>
char *fgets (char *s, int size, FILE *stream);

fgets () 虽然比 gets () 安全,但安全是要付出代价的,代价就是它的使用比 gets () 要麻烦一点,有三个参数。它的功能是从 stream 流中读取 size 个字符存储到字符指针变量 s 所指向的内存空间。它的返回值是一个指针,指向字符串中第一个字符的地址。

8.write () 函数
1
2
3
4
5
6
#include <unistd>
ssize_t write (int filedes, void *buf, size_t nbytes);
// 返回:若成功则返回写入的字节数,若出错则返回 - 1
//filedes:文件描述符
//buf: 待写入数据缓存区
//nbytes: 要写入的字节数
9.read () 函数

实现

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
#include<arpa/inet.h>

int main (int argc, const char* argv [])
{
// 让用户输入端口
if (argc < 2)
{
printf ("eg: ./a.out port\n");
}

int port = atoi (argv [1]);
// 创建套接字
int fd = socket (AF_INET, SOCK_STREAM, 0);

// 连接服务器
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons (port);
//serv.sin_addr.s_addr = htonl ();
inet_pton (AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
connect (fd, (struct sockaddr*)&serv, sizeof (serv));

// 通信
while (1)
{
// 发送数据
char buf [1024];
printf (" 请输入要发送方的字符串:\n")
fgets (buf, sizeof (buf), stdin);
write (fd, buf, strlen (buf));

// 等待接收数据
int len = read (fd, buf, sizeof (buf));
if (len == -1)
{
perror ("read error");
exit (1);
}
else if (len == 0)
{
// 服务器关闭连接,read () 函数就不会阻塞
printf (" 服务器关闭了连接 \n");
break;
}
else
{
printf ("recv buf: % s\n", buf);
}

}

close (fd);
return 0;
}

二、TCP 服务器端编程

三、TCP 三次握手

开启 socket 的时候,操作系统会自动握手;关闭 socket 的时候,操作系统会自动挥手。所以这两个概念只要了解就行了。

三、TCP 四次挥手

四、滑动窗口

五、错误处理函数封装

如果一个函数有部分不符合你的需求,可以自己封装,例如:淘宝封装 nginx

六、TCP 多进程并发服务器

进程和线程的数据共享模式不一样。

七、TCP 多线程并发服务器

C++ advance 文章总览

C++ advance(四)Linux 命令基础 —1.Linux 常用命令
C++ advance(四)Linux 命令基础 —2.vim 和 gcc 和 library
C++ advance(四)Linux 命令基础 —3.makefile 和 gdb 和 IO
C++ advance(四)Linux 命令基础 —4.stat 和 readdir 和 dup2
C++ advance(五)Linux 进程和线程 —1. 进程控制
C++ advance(五)Linux 进程和线程 —2. 进程间通信
C++ advance(五)Linux 进程和线程 —3. 信号
C++ advance(五)Linux 进程和线程 —4. 进程和线程
C++ advance(五)Linux 进程和线程 —5. 线程同步
C++ advance(六)Linux 高并发网络编程开发 —1. 网络编程基础 socket
C++ advance(六)Linux 高并发网络编程开发 —2.tcp 三次握手 - 并发
C++ advance(六)Linux 高并发网络编程开发 —3.tcp 状态转换 - selcet poll
C++ advance(六)Linux 高并发网络编程开发 —4.epoll udp
C++ advance(六)Linux 高并发网络编程开发 —5. 广播 - 组播 - 本地套接字
C++ advance(六)Linux 高并发网络编程开发 —6.libevent
C++ advance(六)Linux 高并发网络编程开发 —7.xml json
C++ advance(七)Linux 高并发 web 服务器开发 —1.
C++ advance(七)Linux 高并发 web 服务器开发 —2.
C++ advance(七)Linux 高并发 web 服务器开发 —3.