首页 C语言 socket编程(一)
文章
取消

C语言 socket编程(一)

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:原始套接字

原始套接字允许对较低层次的协议直接访问,比如IPICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备;

因为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_INETAF_INET6AF_LOCAL(或称AF_UNIX)、AF_ROUTE

协议族决定了socket的地址类型,在通信中必须采用对应的地址。

如AF_INET决定了要用ipv4地址(32位)与端口号(16位)的组合

AF_UNIX决定了要用一个绝对路径名作为地址

常用 type:SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等

常用 protocol:IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_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:输出参数,返回客户端的地址,可为NULL
  • addrlen:输入参数,指定客户端的地址的长度,可为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,指定对应的选项,一般置为0
  • addr:输出参数,保存该数据的发送方地址
  • 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,指定对应的选项,一般置为0
  • addr:输入参数,指定接收方的地址
  • 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握手与挥手

三次握手

tcp三次握手

  1. Client 发送 SYN 包(seq: x),告诉 Server:我要建立连接;Client 进入SYN-SENT状态;

  2. Server 收到 SYN 包后,发送 SYN+ACK 包(seq: y; ack: x+1),告诉它:好的;Server 进入SYN-RCVD状态;

  3. Client 收到 SYN+ACK 包后,发现 ack=x+1,于是进入ESTABLISHED状态,同时发送 ACK 包(seq: x+1; ack: y+1)给 Server;Server 发现 ack=y+1,于是也进入ESTABLISHED状态;

接下来就是互相发送数据、接收数据了……


四次挥手

tcp四次挥手

注意,可以是连接的任意一方主动 close,这里假设 Client 主动关闭连接:

  1. Client 发送 FIN 包,告诉 Server:我已经没有数据要发送了;Client 进入FIN-WAIT-1状态;

  2. Server 收到 FIN 包后,回复 ACK 包,告诉 Client:好的,不过你需要再等会,我可能还有数据要发送;Server 进入CLOSE-WAIT状态;而 Client 收到 ACK 包后,继续等待 Server 做好准备,Client 进入FIN-WAIT-2状态;

  3. Server 准备完毕后,发送 FIN 包,告诉 Client:我也没有什么要发送了,准备关闭连接吧;Server 进入LAST-ACK状态;

  4. 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连接

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
本文由作者按照 CC BY 4.0 进行授权

C语言 断言

C语言 socket编程(二)