socket
网络主机之间的应用程序如何进行通信?
网络层的ip地址
可以唯一标识网络中的主机;
而传输层的协议+端口
可以唯一标识主机中的应用程序(进程);
这样利用三元组(ip地址 + 协议 + 端口)
就可以标识网络的进程
了,网络中的进程通信就可以利用这个标志与其它进程进行交互
什么是socket套接字?
上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?
socket起源于Unix,而Unix/Linux基本哲学之一就是一切皆文件
,都可以用打开open –> 读写read/write –> 关闭close
模式来操作;
socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭);
SOCK_STREAM
:流套接字
流套接字用于提供面向连接
、可靠的数据传输
服务;该服务将保证数据能够实现无差错、无重复发送,并按顺序接收;
流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP协议
SOCK_DGRAM
:数据报套接字
数据报套接字提供了一种无连接
的服务;该服务并不能保证数据传输的可靠性
,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据;
数据报套接字使用UDP协议
进行数据的传输;由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理
SOCK_RAW
:原始套接字
原始套接字允许对较低层次的协议直接访问
,比如IP
、ICMP
协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备;
因为RAW SOCKET可以自如地控制网络底层的传输机制,所以可以应用原始套接字来操纵网络层和传输层应用;
比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包;
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据包套接字)的区别在于:
原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据;因此,如果要访问其他协议发送数据必须使用原始套接字
socket API
socket
int socket(int domain, int type, int protocol);
:创建一个socket文件描述符fd
domain
:输入参数,协议域、地址域或协议族
。type
:输入参数,socket类型;protocol
:输入参数,指定使用的协议;- 返回值:成功返回一个非负整数的fd,失败则返回-1,并设置errno
常用 domain:AF_INET
、AF_INET6
、AF_LOCAL
(或称AF_UNIX
)、AF_ROUTE
协议族决定了socket的地址类型,在通信中必须采用对应的地址。
如AF_INET决定了要用ipv4地址(32位)与端口号(16位)的组合
AF_UNIX决定了要用一个绝对路径名作为地址
常用 type:SOCK_STREAM
、SOCK_DGRAM
、SOCK_RAW
、SOCK_PACKET
、SOCK_SEQPACKET
等等
常用 protocol:IPPROTO_TCP
、IPPTOTO_UDP
、IPPROTO_SCTP
、IPPROTO_TIPC
等等
如果该参数为0,则让系统自动选择合适的协议,一般我们也这么操作
bind
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族空间中,但没有一个具体的地址;
如果想要给它赋值一个地址,就必须调用bind()
函数,否则就当调用connect()
、listen()
时系统会自动随机分配一个端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
:绑定一个固定的socket地址
sockfd
:输入参数,socket文件描述符,即socket()的返回值addr
:输入参数,指向sock地址的指针addrlen
:输入参数,sock地址的长度- 返回值:成功返回0,失败返回-1,并设置errno
listen
int listen(int sockfd, int backlog);
:监听socket套接字
sockfd
:输入参数,被监听的套接字backlog
:输入参数,最大等待队列数量,宏SOMAXCONN
为系统设定的最大值,可通过/etc/sysctl.conf
修改- 返回值:成功返回0,失败返回-1,并设置errno
connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
:连接socket套接字
sockfd
:输入参数,客户端的套接字addr
:输入参数,服务器的地址addrlen
:输入参数,服务器的地址的长度- 返回值:成功返回0,失败返回-1,并设置errno
accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
:接受socket连接请求
sockfd
:输入参数,被监听的套接字addr
:输出参数,返回客户端的地址,可为NULLaddrlen
:输入参数,指定客户端的地址的长度,可为NULL- 返回值:成功返回已连接的新套接字描述符connfd,失败返回-1,并设置errno
recv
int recv(int sockfd, void *buf, int len, int flags);
:从socket接收数据
sockfd
:输入参数,从这个socket接收数据buf
:输出参数,用来保存接收到的数据len
:输入参数,指定buf的长度,表示最多接收这么多个字节的数据flags
:输入参数,flags,指定对应的选项,一般置为0- 返回值:成功返回接收到的数据大小,返回0表示对方不再发送数据(可以理解为关闭了连接),出错返回-1,并设置errno
send
int send(int sockfd, void *buf, int len, int flags);
:向socket发送数据
sockfd
:输入参数,向这个socket发送数据buf
:输入参数,发送buf指向的数据len
:输入参数,指定buf的长度,指定要发送的数据大小flags
:输入参数,flags,指定对应的选项,一般置为0- 返回值:成功返回已发送的数据大小,失败则返回-1,并设置errno
recvfrom
int recvfrom(int sockfd, void *buf, int len, int flags, struct sockaddr *addr, socklen_t *addrlen);
:从udp socket接收数据
sockfd
:输入参数,从该socket接收数据buf
:输出参数,将接收的数据存放在buf上len
:输入参数,指定buf的长度flags
:输入参数,flags,指定对应的选项,一般置为0addr
:输出参数,保存该数据的发送方地址addrlen
:输入参数,指定发送方地址的长度- 返回值:成功返回接收到的数据大小,失败则返回-1,并设置errno
sendto
int sendto(int sockfd, void *buf, int len, int flags, struct sockaddr *addr, socklen_t addrlen);
:向udp socket发送数据
sockfd
:输入参数,向该socket发送数据buf
:输入参数,发送buf指向的数据len
:输入参数,指定buf的长度flags
:输入参数,flags,指定对应的选项,一般置为0addr
:输入参数,指定接收方的地址addrlen
:输入参数,指定接收方的地址的长度- 返回值:成功返回发送的数据大小,失败则返回-1,并设置errno
最后一个参数flags
:
MSG_WAITALL
:用于recv,尽可能等待所有数据MSG_DONTWAIT
:用于recv、send,仅本次操作不阻塞MSG_DONTROUTE
:用于send,绕过路由表查找MSG_OOB
:用于recv、send,发送或接收带外数据MSG_PEEK
:用于recv,窥看外来数据,不将其从socket缓冲区中删除
shutdown
int shutdown(int sockfd, int howto);
:关闭tcp连接
sockfd
:输入参数,即将关闭连接的套接字howto
:输入参数,定义如何关闭:SHUT_RD
值为0,关闭读、SHUT_WR
值为1,关闭写、SHUT_RDWR
值为2,关闭读写- 返回值:成功返回0,失败返回-1,并设置errno
close
int close(int fd);
:关闭socket
fd
:输入参数,要关闭的文件描述符fd- 返回值:成功返回0,失败返回-1,并设置errno
shutdown和close区别
shutdown()
函数用于tcp连接的socket套接字,对udp无效
用于更精细的控制流的读和写,可以只关闭读,也可以只关闭写,或者读写都关闭,但是关闭后,该sockfd依旧是有效的,仍需调用close进行关闭
而close()
函数用于关闭对应的fd文件描述符,socket也是特殊的文件,注意,close只是将对应的sockfd的引用计数减1,当引用计数减到0时,系统才关闭该sockfd
还有一点:在多进程程序中,使用shutdown会导致共享进程的连接也被关闭,读写出现错误,而close不会影响共享进程的socket
tcp握手与挥手
三次握手
Client 发送 SYN 包(seq: x),告诉 Server:我要建立连接;Client 进入
SYN-SENT
状态;Server 收到 SYN 包后,发送 SYN+ACK 包(seq: y; ack: x+1),告诉它:好的;Server 进入
SYN-RCVD
状态;Client 收到 SYN+ACK 包后,发现 ack=x+1,于是进入
ESTABLISHED
状态,同时发送 ACK 包(seq: x+1; ack: y+1)给 Server;Server 发现 ack=y+1,于是也进入ESTABLISHED
状态;
接下来就是互相发送数据、接收数据了……
四次挥手
注意,可以是连接的任意一方主动 close,这里假设 Client 主动关闭连接:
Client 发送 FIN 包,告诉 Server:我已经没有数据要发送了;Client 进入
FIN-WAIT-1
状态;Server 收到 FIN 包后,回复 ACK 包,告诉 Client:好的,不过你需要再等会,我可能还有数据要发送;Server 进入
CLOSE-WAIT
状态;而 Client 收到 ACK 包后,继续等待 Server 做好准备,Client 进入FIN-WAIT-2
状态;Server 准备完毕后,发送 FIN 包,告诉 Client:我也没有什么要发送了,准备关闭连接吧;Server 进入
LAST-ACK
状态;Client 收到 FIN 包后,知道 Server 准备完毕了,于是给它回复 ACK 包,告诉它我知道了,于是进入
TIME-WAIT
状态;而 Server 收到 ACK 包后,即进入CLOSED
状态;Client 等待 2MSL 时间后,没有再次收到 Server 的 FIN 包,于是确认 Server 收到了 ACK 包并且已关闭,于是 Client 也进入CLOSED
状态;
MSL
即报文最大生存时间
,RFC793 中规定 MSL 为 2 分钟,实际应用中常用的是 30 秒、1 分钟、2 分钟等;可以修改/etc/sysctl.conf
内核参数,来缩短TIME_WAIT
的时间,避免不必要的资源浪费。
最后附上tcp从建立连接到传输数据到释放连接的过程图:
tcp_socket实例
实现一个echo回声的服务端/客户端,即client发送一条消息给server,server就把该消息原样返回给client:
tcp_echo_server.c
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
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#define LISTEN_PORT 8080
#define BUF_SIZE 128
int main(void){
int listenfd;
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
perror("create_listenfd error");
exit(EXIT_FAILURE);
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(LISTEN_PORT);
if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0){
perror("bind_listenfd error");
exit(EXIT_FAILURE);
}
if(listen(listenfd, SOMAXCONN) < 0){
perror("listen_listenfd error");
exit(EXIT_FAILURE);
}
int connfd;
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
char buf[BUF_SIZE];
int nbuf;
for(;;){
if((connfd = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0){
perror("accept_listenfd error");
continue;
}
nbuf = recv(connfd, buf, BUF_SIZE, 0);
buf[nbuf] = 0;
if(!strcmp(buf, "exit")){
printf("exit_server\n");
close(connfd);
break;
}
printf("new conn(%s:%d); msg: %s\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);
send(connfd, buf, nbuf, 0);
close(connfd);
}
close(listenfd);
return 0;
}
tcp_echo_client.c
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
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#define SERV_PORT 8080
#define BUF_SIZE 128
int main(int argc, char *argv[]){
if(argc < 2){
fprintf(stderr, "usage: %s <MSG>\n", argv[0]);
exit(EXIT_FAILURE);
}
int sockfd;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
perror("create_sockfd error");
exit(EXIT_FAILURE);
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
if(inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr) <= 0){
perror("convert_servaddr error");
exit(EXIT_FAILURE);
}
servaddr.sin_port = htons(SERV_PORT);
if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0){
perror("connect_sockfd error");
exit(EXIT_FAILURE);
}
char buf[BUF_SIZE];
int nbuf = strlen(argv[1]);
send(sockfd, argv[1], nbuf, 0);
nbuf = recv(sockfd, buf, BUF_SIZE, 0);
buf[nbuf] = 0;
printf("echo msg: %s\n", buf);
close(sockfd);
return 0;
}
测试echo回声程序:
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
$ gcc -o server tcp_echo_server.c
$ gcc -o client tcp_echo_client.c
$ ./server
$ for ((i=0; i<10; i++)); do ./client 'www.zfl9.com'; done && ./client 'exit'
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg:
$ ./server
new conn(127.0.0.1:55806); msg: www.zfl9.com
new conn(127.0.0.1:55808); msg: www.zfl9.com
new conn(127.0.0.1:55810); msg: www.zfl9.com
new conn(127.0.0.1:55812); msg: www.zfl9.com
new conn(127.0.0.1:55814); msg: www.zfl9.com
new conn(127.0.0.1:55816); msg: www.zfl9.com
new conn(127.0.0.1:55818); msg: www.zfl9.com
new conn(127.0.0.1:55820); msg: www.zfl9.com
new conn(127.0.0.1:55822); msg: www.zfl9.com
new conn(127.0.0.1:55824); msg: www.zfl9.com
exit_server
udp_socket实例
我们也实现一个echo回声:
udp_echo_server.c
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
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#define BIND_PORT 8080
#define BUF_SIZE 128
int main(void){
int sockfd;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0){
perror("create_sockfd error");
exit(EXIT_FAILURE);
}
struct sockaddr_in bindaddr;
memset(&bindaddr, 0, sizeof(bindaddr));
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(BIND_PORT);
if(bind(sockfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) < 0){
perror("bind_sockfd error");
exit(EXIT_FAILURE);
}
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
char buf[BUF_SIZE];
int nbuf;
for(;;){
nbuf = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&peeraddr, &peerlen);
buf[nbuf] = 0;
if(!strcmp(buf, "exit")){
printf("exit_server\n");
break;
}
printf("new msg(%s:%d): %s\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);
sendto(sockfd, buf, nbuf, 0, (struct sockaddr *)&peeraddr, peerlen);
}
close(sockfd);
return 0;
}
udp_echo_client.c
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
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#define SERV_PORT 8080
#define BUF_SIZE 128
int main(int argc, char *argv[]){
if(argc < 2){
fprintf(stderr, "usage: %s <MSG>\n", argv[0]);
exit(EXIT_FAILURE);
}
int sockfd;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0){
perror("create_sockfd error");
exit(EXIT_FAILURE);
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
if(inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr) <= 0){
perror("convert_servaddr error");
exit(EXIT_FAILURE);
}
servaddr.sin_port = htons(SERV_PORT);
char buf[BUF_SIZE];
int nbuf = strlen(argv[1]);
sendto(sockfd, argv[1], nbuf, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
nbuf = recvfrom(sockfd, buf, BUF_SIZE, 0, NULL, NULL);
buf[nbuf] = 0;
printf("echo msg: %s\n", buf);
close(sockfd);
return 0;
}
测试echo回声程序:
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
$ gcc -o server udp_echo_server.c
$ gcc -o client udp_echo_client.c
$ ./server
$ ./client 'test'
^C
$ for ((i=0; i<10; i++)); do ./client 'www.zfl9.com'; done && ./client 'exit'
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
echo msg: www.zfl9.com
^C
$ ./server
new msg(127.0.0.1:45884): test
new msg(127.0.0.1:47339): www.zfl9.com
new msg(127.0.0.1:49160): www.zfl9.com
new msg(127.0.0.1:34802): www.zfl9.com
new msg(127.0.0.1:45306): www.zfl9.com
new msg(127.0.0.1:59937): www.zfl9.com
new msg(127.0.0.1:53763): www.zfl9.com
new msg(127.0.0.1:51414): www.zfl9.com
new msg(127.0.0.1:58480): www.zfl9.com
new msg(127.0.0.1:35993): www.zfl9.com
new msg(127.0.0.1:33064): www.zfl9.com
exit_server