一、套接字socket

1.1 概念

1> 最早的套接字也是跟消息队列、共享内存、管道一样,只能实现同一主机之间的多个进程间的通信

2> 随着tcp/ip协议族的出现,使得消息能够穿过网卡设备,在网络中进行传输

3> 套接字通信使用的是套接字文件,也是一种特殊的文件,bcd-lsp中的s说的就是该文件

4> socket这个函数,用于创建一个套接字文件,套接字文件的通信原理如下所示

img

1.2 socket函数介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
       #include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
功能:创建一个用于通信的端点,并返回该通信对应的文件描述符,描述符使用最小未分配原则
参数1:通信域对应的协议族
AF_UNIX, AF_LOCAL Local communication(本地通信) unix(7) man 7 unix可以查看相信信息
AF_INET IPv4 Internet protocols(IPv4通信) ip(7) man 7 ip可以查看相信信息
AF_INET6 IPv6 Internet protocols(IPv6通信) ipv6(7) man 7 ipv6可以查看相信信息
参数2:指定通信语义,理解成传输方式
SOCK_STREAM 提供支持TCP通信

SOCK_DGRAM 提供支持UDP通信
SOCK_RAW 通过原始的套接字通信
参数3:通信协议,如果参数2指定了确定的通信方式,该参数填0即可
如果不确定通信方式,可用的参数有:
TCP:IPPROTO_TCP
UDP:IPPROTO_UDP
返回值:成功返回套接字文件描述符,失败返回-1并置位错误码

二、基于TCP的基本通信

2.1 通信流程

img

2.2 服务器端相关API

1> 创建套接字:

该套接字用于接收客户端的连接请求使用,并不是用于通信的套接字

2> bind绑定端口号和IP地址

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
       #include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:给指定的套接字文件描述符绑定IP地址和端口号
参数1:要绑定的套接字文件描述符
参数2:地址信息结构体,包含了通信域、IP地址、端口号
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
以上结构体是通用地址信息结构体,无论TCP还是UDP都可以使用,主要用于形参的强制类型转换,避免出现编译报错,但是具体的通信族有不同的地址信息结构体
对于IPv4通信,需要man 7 ip进行查看
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */ 通信域
in_port_t sin_port; /* port in network byte order */ 端口号,网络字节序
struct in_addr sin_addr; /* internet address */ 网络地址,是一个结构体
};
struct in_addr {
uint32_t s_addr; /* address in network byte order */ IP地址,网络字节序
};

对于本地的通信,需要man 7 unix查看
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */ 标识域套接字
char sun_path[108]; /* pathname */ 套接字文件的名字
};
参数3:参数2的大小
返回值: 成功返回0,失败返回-1并置位错误码



3> listen设置监听

1
2
3
4
5
6
7
8
       #include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);
功能:将套接字文件描述符设置成监听状态,以监听客户端的连接请求
参数1:服务器套接字文件描述符
参数2:挂起队列的长度,当该队列长度满时,其他连接的客户端将会报错,一般设置为128
返回值: 成功返回0,失败返回-1并置位错误码

4> accept阻塞等待客户端连接请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
       #include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:阻塞接收客户端的连接请求,并给该客户端创建一个新的用于通信的套接字
参数1:用于连接的套接字文件描述符
参数2:用于接收客户端的地址信息的结构体指针,如果不愿意接收,填NULL即可
参数3:接收客户端的地址信息的长度,是一个指针,也需要传递变量的地址
返回值:成功返回一个用于通信的套接字文件描述符失败返回-1并置位错误码

注意:
1、bind error: Address already in use:说明端口号已经被占用
2、bind error: Cannot assign requested address:说明IP地址填写错误

5> 数据收发函数:send、recv

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
       #include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:向套接字文件描述符中发送数据
参数1:要发送的套接字文件描述符
参数2:要发送数据的起始地址
参数3:要发送数据的大小
参数4:标识是否阻塞
0:表示阻塞
MSG_DONTWAIT:表示非阻塞
返回值:成功返回发送的字节个数,失败返回-1并置位错误码


#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:从给的的套接字文件描述符中读取数据
参数1;要读取的套接字文件描述符
参数2:读取数据的容器地址
参数3:读取的大小
参数4: 标识是否阻塞
0:表示阻塞
MSG_DONTWAIT:表示非阻塞
返回值:
>0:表示接收的字符的个数
=0:表示对方已经退出
=-1:出错,置位错误码

6> 服务器端实现代码

img

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
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.125.78" //服务器客户端


int main(int argc, const char *argv[])
{
//1、创建用于连接的套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
//参数1:通信域,表明使用的是ipv4协议
//参数2:通信方式,使用TCP通信
//参数3:0表示之前已经指定协议 IPPROTO_TCP

if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("sfd = %d\n", sfd); //3


//将端口号快速重用函数
int reuse = 1;
if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("端口号快速重用成功\n");



//2、给当前套接字绑定IP地址和端口号
//2.1填充要绑定的地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; //通信域
sin.sin_port = htons(SER_PORT); //端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); //ip地址

//2.2 绑定
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success %s %s %d\n", __FILE__, __func__, __LINE__);

//3、将套接字设置成监听状态
if(listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success %s %s %d\n", __FILE__, __func__, __LINE__);

//4、阻塞等待客户端的链接请求
//4.1定义容器接收客户端的地址信息
struct sockaddr_in cin; //用于接收地址信息
socklen_t socklen = sizeof(cin); //用于接收地址信息的大小

int newfd = -1;
//4.2 接收客户端的链接
newfd = accept(sfd, (struct sockaddr*)&cin, &socklen);
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]发来链接请求 %s %s %d\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port),__FILE__, __func__, __LINE__);

//5、跟客户端进行消息通信
char buf[128] = "";
while(1)
{
//将数组清空
bzero(buf, sizeof(buf));

//读取客户端发来的消息
//int res = read(newfd, buf, sizeof(buf));
int res = recv(newfd, buf, sizeof(buf), 0);
if(res == 0)
{
printf("客户端已经下线\n");
break;
}
printf("[%s:%d] : %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf);

//给客户端发消息
strcat(buf, "*_*");

//write(newfd, buf, sizeof(buf));
send(newfd, buf, sizeof(buf), 0);
printf("发送成功\n");
}

//6、关闭套接字
close(newfd);
close(sfd);





return 0;
}

2.3 客户端端相关API及实现

1> socket:创建一个用于通信的套接字文件描述符

2> bind:可绑定也可以不绑定

3> connect:连接服务器

4> send\recv: 通信

5> close:关闭套接字

1
2
3
4
5
6
7
8
9
       #include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将指定的客户端套接字,连接到服务器
参数1;客户端套接字文件描述符
参数2:服务器的地址信息结构体
参数3:参数2的大小
返回值:成功返回0,失败返回-1并置位错误码

6> 客户端的实现

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
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.125.78" //服务器ip地址
#define CLI_PORT 6666 //客户端的端口号
#define CLI_IP "192.168.125.78" //客户端ip地址


int main(int argc, const char *argv[])
{
//1、创建用于通信的套接字文件描述符
int cfd = -1;
cfd = socket(AF_INET, SOCK_STREAM, 0);
if(cfd == -1)
{
perror("socket error");
return -1;
}
printf("cfd = %d\n", cfd); //3

//2、绑定(可选)
//2.1 填充地址信息结构体
struct sockaddr_in cin;
cin.sin_family = AF_INET;
cin.sin_port = htons(CLI_PORT);
cin.sin_addr.s_addr = inet_addr(CLI_IP);
//2.2绑定
if(bind(cfd, (struct sockaddr*)&cin, sizeof(cin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");

//3、连接服务器
//3.1 填充服务器地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT);
sin.sin_addr.s_addr = inet_addr(SER_IP);
//3.2 连接
if(connect(cfd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("connect error");
return -1;
}
printf("connect success\n");


//4、收发数据
char buf[128] = "";
while(1)
{
//清空数组
bzero(buf, sizeof(buf));

printf("请输入>>>");
fgets(buf, sizeof(buf), stdin); //从终端输入数据
buf[strlen(buf)-1] = 0; //将换行改为'\0'

//发送给服务器
send(cfd, buf, sizeof(buf), 0);
printf("发送成功\n");
if(strcmp(buf, "quit") == 0)
{
break;
}

//接收服务器发来的消息
recv(cfd, buf, sizeof(buf), 0);
printf("[%s:%d]:%s\n", SER_IP, SER_PORT, buf);
}

//5、关闭套接字
close(cfd);

return 0;
}

三、基于UDP的基础通信

3.1 通信模型

img

3.2 UDP服务器模型

1> socket:创建用于通信的套接字文件描述符

2> bind:绑定端口号和IP地址

3> sendto/recvfrom:数据收发

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
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
功能:读取客户端发来的消息
参数1:当前套接字文件描述符
参数2:存放数据的容器起始地址
参数3:读取数据的大小
参数4:是否阻塞
0:表示阻塞
MSG_DONTWAIT:表示非阻塞
参数5:接受客户端地址信息结构体的容器起始地址
参数6:参数5的大小
返回值:
>=0:返回读取的字节个数
=-1:失败,置位错误码



ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
功能:向指定的套接字文件描述符中发送数据

参数1:当前套接字文件描述符
参数2:要发送数据的容器起始地址
参数3:发送数据的大小
参数4:是否阻塞
0:表示阻塞
MSG_DONTWAIT:表示非阻塞
参数5:接受消息端地址信息结构体的容器起始地址
参数6:参数5的大小
返回值:成功返回发送字节的个数,失败返回-1并置位错误码

4> close:关闭客户端

5> 通信模型实现

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<myhead.h>
#define SER_PORT 9999
#define SER_IP "192.168.125.78"

int main(int argc, const char *argv[])
{
//1、创建套接字
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
//SOCK_DGRAM:表示使用UDP报式套接字通信
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("sfd = %d\n", sfd);


//2、绑定
//2.1 填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT); //端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); //ip地址
//2.2 绑定工作
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}

//3、数据收发
char buf[128] = "";
//定义变量存放客户端地址信息结构体
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);


while(1)
{
//清空数组
bzero(buf, sizeof(buf));

//从套接字文件中读取消息
//read(sfd, buf, sizeof(buf));
recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &socklen);
//功能:读取消息
//并将客户端地址信息结构体存储到cin中

printf("读取的消息为:%s\n", buf);

//将字符串连接后回发
strcat(buf, "*_*");
//if(write(sfd, buf, sizeof(buf)) == -1)
if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, sizeof(cin)) == -1)
{
perror("write error");
return -1;
}
printf("发送成功\n");
}

//4、关闭套接字
close(sfd);

return 0;
}

3.3 UDP客户端实现

1> socket 创建套接字

2> bind 可以绑定也可以不绑定

3> 数据收发

4> 关闭套接字

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<myhead.h>
#define SER_PORT 9999
#define SER_IP "192.168.125.104"

int main(int argc, const char *argv[])
{
//1、创建用于通信的套接字文件描述符
int cfd = socket(AF_INET, SOCK_DGRAM, 0);
if(cfd == -1)
{
perror("socket error");
return -1;
}

//2、绑定(可选)

//3、填充服务器的地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT);
sin.sin_addr.s_addr = inet_addr(SER_IP);

//4、数据收发
char buf[128] = "";
while(1)
{
printf("请输入>>>");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = '\0';

//发送给服务器
sendto(cfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin));

if(strcmp(buf, "quit") == 0)
{
break;
}

//接收服务器发送来的消息
recvfrom(cfd, buf, sizeof(buf), 0, NULL, NULL);
printf("收到消息:%s\n", buf);


}

//5、关闭套接字
close(cfd);


return 0;
}

3.4 udp中使用connect函数(笔试面试题)

1> udp中可以使用connect函数,但是不会产生三次握手

2> udp中一般在服务器端使用connect函数。

3> udp中如果使用的connect函数,那么该服务器将与指定的客户端会建立唯一的通道,其他客户端向该服务器发消息,就不再接收了

4> udp中可以多次使用connect函数,连接不同的客户端进行通信,也可以设置都不连接,需要将sin_family成员设置成AF_UNSPCE再次进行连接函数

5> 当服务器跟某个客户端连接后,就可以像TCP一样完成使用read/write、send/recv函数了

6> UDP中使用connect的好处

提高传输效率:

不连接时:将信息填充到内核 —> 发送数据 —>清空内核数据 —> 将信息填充到内核 —> 发送数据 —>清空内核数据

连接后:将信息填充到内核 —> 发送数据 —> 发送数据 —> 发送数据 —> 发送数据 —> 发送数据

提高数据传输的稳定性:

当服务器连接了A客户端后,建立了唯一的通道,其他客户端再发消息就不接受了,不会影响到A客户端与服务器的沟通

如果不连接,可能会存在A客户端向服务器发送消息过程中,B客户端也向服务器发消息

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<myhead.h>
#define SER_PORT 9999
#define SER_IP "192.168.125.104"

int main(int argc, const char *argv[])
{
//1、创建套接字
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
//SOCK_DGRAM:表示使用UDP报式套接字通信
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("sfd = %d\n", sfd);


//2、绑定
//2.1 填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT); //端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); //ip地址
//2.2 绑定工作
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}

//3、数据收发
char buf[128] = "";
//定义变量存放客户端地址信息结构体
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);


while(1)
{
//清空数组
bzero(buf, sizeof(buf));

//从套接字文件中读取消息
//read(sfd, buf, sizeof(buf));
recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &socklen);
//功能:读取消息
//并将客户端地址信息结构体存储到cin中

//将服务器与第一个客户端建立连接
if(strcmp(buf,"connect")==0)
{
connect(sfd, (struct sockaddr*)&cin, sizeof(cin));

send(sfd, buf, sizeof(buf), 0); //回个消息

}else if(strcmp(buf,"disconnect")==0)
{
//断开连接
cin.sin_family = AF_UNSPEC;
connect(sfd, (struct sockaddr*)&cin, sizeof(cin));

}

printf("读取的消息为:%s\n", buf);

//将字符串连接后回发
strcat(buf, "*_*");
//if(write(sfd, buf, sizeof(buf)) == -1)
if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, sizeof(cin)) == -1)
{
perror("write error");
return -1;
}
printf("发送成功\n");
}

//4、关闭套接字
close(sfd);

return 0;
}

本章完