Linux Socket网络编程: TCP/UDP与本地套接字

网络交互和数据传输好比打电话,socket就像电话机,是在网络编程世界中与外界进行网络通信的途径

TCP网络编程

基于服务器-客户端模型,使用套接字完成连接的建立

服务端准备连接

使用socket创建一个可用的套接字:

NAME
       socket - create an endpoint for communication

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

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

domain表示套接字域,type参数设定为SOCK_STREAM就表示为字节流,对应于TCP,protocol参数指定为0

创建套接字后要将其和套接字和套接字和地址绑定:

NAME
       bind - bind a name to a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

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

函数的地一个参数是上述的套接字文件描述符,第二个参数是通用地址格式sockaddr,第三个参数是地址长度

设置bind的参数时,对地址和端口有多种处理方式,可以将设置成本机IP地址,这相当于告诉操作系统内核,仅仅对目标IP是本机IP地址的IP包进行处理。但是将程序部署时有一个问题: 开发者并不清楚程序将会被部署到哪一台机器上。此时设置通配地址,对于IPv4地址来说,使用INADDR_ANY配置通配地址,IPv6则是IN6ADDR_ANY完成设置

如下函数创建了一个套接字绑定地址和端口号并返回:

int create_socket(uint16_t port)
{
    int sock;
    struct sockaddr_in addr;

    // create socket
    sock = socket(AF_INET,SOCK_STREAM,0);
    if (sock < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port); // 主机字节序和网络字节序的转换
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    // bind address:port
    if (bind(sock,(struct sockaddr *)&addr,sizeof(addr)) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    return sock;
}

监听

使用listen函数等待用户请求,操作系统会为此做好接收用户全球的一切准备,比如完成准备队列

NAME
       listen - listen for connections on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int listen(int sockfd, int backlog);

第一个参数是sockfd为套接字描述符,第二个参数backlog决定了可以接收的并发数目,这个参数越大,并发数目理论上也会越大

应答

当客户端的连接请求到达时,服务器端应答成功,建立连接,accept函数看成是操作系统内核和应用程序之间的桥梁

NAME
       accept, accept4 - accept a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

第一个参数sockfd是套接字,是前面通过bind和listen一系列操作得到的套接字,该函数的返回值有两个部分,第一个部分是addr通过指针方式获取的客户端的地址,addrlen显示地址的大小,第二个是返回已连接套接字描述符

这里将监听套接字和已连接套接字分开,因为网络程序的并发特征。监听套接字是一直都存在的,直到这个套接字关闭,而一旦一个客户与服务器连接成功,完成了TCP三次握手,操作系统内核就为这个客户生成一个已连接套接字,让应用服务器使用这个已连接套接字和客户进行通信处理。如果客户关闭连接,那么释放的是已连接套接字,这样就完成了TCP的释放。监听套接字依然还处于"监听"状态

TCP服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>

int create_socket(uint16_t port)
{
    int sock;
    struct sockaddr_in addr;

    // create socket
    sock = socket(AF_INET,SOCK_STREAM,0);
    if (sock < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    // bind address:port
    if (bind(sock,(struct sockaddr *)&addr,sizeof(addr)) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    return sock;
}

int main(void)
{
    int serv_sock = create_socket(8090);
    if (listen(serv_sock,5) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("listening at port 8090...\n");
    
    while (true) {
        struct sockaddr_in clnt_addr = {0};
        socklen_t len = sizeof(clnt_addr);
        int clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_addr,&len);
        if (clnt_sock < 0) {
            perror("connect");
            continue;
        } else {
            printf("%s:%d is connecting\n",inet_ntoa(clnt_addr.sin_addr),ntohs(clnt_addr.sin_port));
        }
        close(clnt_sock);
    }
    
    close(serv_sock);

    return 0;
}

inet_ntoa的作用是类型转换,ntohs将网络字节序转化为主机字节序

客户端发起连接

客户端同样须要建立一个套接字,方法是一样的,但客户端是通过connect函数发起请求

NAME
       connect - initiate a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

第一个参数是sockfd是连接套接字,通过socket创建。第二个和第三个参数代表指向套接字地址结构的指针和该结构的大小,套接字地址结构必须含有服务器的IP地址和端口号,客户端在调用函数connect前不必调用bind函数,因为如果需要的话,内核会确定IP地址,并自行确定端口号

对于TCP套接字,connect函数将触发TCP的3次握手过程,出错返回可能有以下几种情况:

  1. 三次握手无法建立,客户端发出的SYN包没有任何响应,返回TIMEOUT错误
  2. 客户端收到RST复位应答,这时候客户端会立即返回CONNECTION REFUSED错误,这种情况比较常见于客户端发送连接请求时的请求端口写错,因为 RST 是 TCP 在发生错误时发送的一种 TCP 分节,产生RST的3个条件是:目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务;TCP 想取消一个已有连接;TCP 接收到一个根本不存在的连接上的分节
  3. 客户发出的 SYN 包在网络上引起了"destination unreachable",即目的不可达的错误。这种情况比较常见的原因是客户端和服务器端路由不通

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    int clnt_sock = socket(PF_INET,SOCK_STREAM,0);
    if (clnt_sock < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    
    struct sockaddr_in serv_addr;
    inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr);
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(8090);

    if (connect(clnt_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) < 0) {
        perror("connect");
        exit(EXIT_FAILURE);
    }

    printf("connect succeed\n");
    close(clnt_sock);
    return 0;
}

运行测试

开启服务器:

$ gcc tcp_server.c -o tcp_server && ./tcp_server 
listening at port 8090...

运行客户端:

$ gcc tcp_client.c -o tcp_client && ./tcp_client 
connect succeed

服务端打印信息:

$ gcc tcp_server.c -o tcp_server && ./tcp_server 
listening at port 8090...
127.0.0.1:55052 is connecting

wireshakr抓包:

上述7条记录分别对应了3次握手和4次挥手,前3条记录就是3次握手:

img

3次握手具体的过程:

  1. 客户端协议栈向服务器端发送了SYN包,并告诉服务端当前序列号j,客户端进入SYNC_SENT状态

  1. 服务端的协议栈收到这个包之后,和客户端进入ACK应答,应答的值为j+1,表示对SYN包j的确认,同时服务器也发送一个SYN包,告诉客户端当前发送序列号为k,服务器端进入SYNC_RCVD状态

  1. 客户端协议栈收到ACK之后,使得应用程序从connect调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为ESTABLISHED,同时客户端协议栈也会对服务器端的SYN包进行应答,应答数据为k+1

UDP网络编程

UDP是一种"数据报"协议,TCP是一种面向连接的"数据流"协议。TCP在IP报文的基础上增加了诸如重传、确认、有序传输、拥塞控制等能力,通信的双方是在一个确定的上下文中工作的。而UDP没有一个确定的上下文,是一个不可靠的通信协议,没有重传和确认,没有有序控制,也没有拥塞控制。

img

服务端

服务器端创建 UDP 套接字之后,绑定到本地端口,调用 recvfrom 函数等待客户端的报文发送

服务端代码:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>

#define SERV_PORT 8090

int main(void)
{
    int serv_sock = socket(AF_INET,SOCK_DGRAM,0);
    if (serv_sock < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in serv_addr = {0};
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(SERV_PORT);

    if (bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    char message[1024] = {0};

    struct sockaddr_in clnt_addr = {0};
    socklen_t len = sizeof(clnt_addr);
    while (true) {
        int n = recvfrom(serv_sock,message,1024,0,(struct sockaddr*)&clnt_addr,&len);
        message[n] = 0;
        printf("received %d bytes: %s from %s:%u\n",n,message,
            inet_ntoa(clnt_addr.sin_addr),ntohs(clnt_addr.sin_port));
        strcpy(message,"receive OK");
        sendto(serv_sock,message,sizeof(message),
            0,(struct sockaddr*)&clnt_addr,len
        );
    }

    return 0;
}  

如上代码,服务端调用recvfrom接收发送来的数据,调用sento向客户端发送数据

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
		struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
        const struct sockaddr *dest_addr, socklen_t addrlen);

这两个函数的前4个参数相同,分别是本地创建的套接字描述符,数据缓冲区,缓冲区最大长度,标志,对于recvfrom来说后2个参数用于利用指针获取发送方的地址信息,对于sento来说是接收方的地址信息和长度,表示要发给谁。在服务端程序中,这个地址就是客户端的地址,服务端通过recvfrom函数拿到发送数据的客户端的地址信息,传递给sendto用于发送数据

客户端

读取输入的字符串后,发送给服务端,并且把服务端经过处理的报文打印到标准输出上

代码:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>


#define SERV_PORT 8090

int main(void)
{
    int clnt_sock = socket(AF_INET,SOCK_DGRAM,0);
    if (clnt_sock < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in serv_addr = {0};
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    socklen_t len = sizeof(serv_addr);
    char send_buff[1024] = {0};
    char recv_buff[1024] = {0};

    while (true) {
        scanf("%s",send_buff);
        // send message
        sendto(clnt_sock,send_buff,sizeof(send_buff),0,
            (struct sockaddr*)&serv_addr,len);
        // recv message
        int ret = recvfrom(clnt_sock,recv_buff,255,0,
                (struct sockaddr*)&serv_addr,&len);
        
        if (ret > 0) {
            recv_buff[ret] = 0;
            printf("recv %d bytes:%s from %s:%d\n",ret,recv_buff,
                inet_ntoa(serv_addr.sin_addr),ntohs(serv_addr.sin_port));
        }
    }

    return 0;    
}

运行测试

客户端发送数据:

$ ./udp_client
Hello!
recv 255 bytes:receive OK from 127.0.0.1:8090

服务端接收数据:

$ ./udp_server 
received 1024 bytes: Hello! from 127.0.0.1:43968

本地套接字

本地套接字一般也叫做 UNIX 域套接字,本地套接字是 IPC,也就是本地进程间通信的一种实现方式。除了本地套接字以外,其它技术,诸如管道、共享消息队列等也是进程间通信的常用方法,但因为本地套接字开发便捷,接受度高,所以普遍适用于在同一台主机上进程间通信的各种场景。本地套接字是一种特殊类型的套接字,和 TCP/UDP 套接字不同。TCP/UDP 即使在本地地址通信,也要走系统网络协议栈,而本地套接字,严格意义上说提供了一种单主机跨进程间调用的手段,减少了协议栈实现的复杂度,效率比 TCP/UDP 套接字都要高许多。类似的 IPC 机制还有 UNIX 管道、共享内存和 RPC 调用等

本地字节流套接字

服务器打开本地套接字后,接收客户端发送来的字节流,并往客户端回送了新的字节流

服务端

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main(int argc,char *argv[])
{   
    if (argc != 2) {
        error(1,0,"usage: unixstreamserver <local_path>");
    }
	
    // 监听套接字&连接套接字
    int listenfd,connfd;
    socklen_t clilen;
    struct sockaddr_un cliaddr,servaddr;

    listenfd = socket(AF_LOCAL,SOCK_STREAM,0); // 套接字类型
    if (listenfd < 0) {
        error(1,errno,"socket created failed"); 
    }

    char *local_path = argv[1]; // 地址
    unlink(local_path); // 删除路径
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path,local_path); // 设置本地文件路径
	
    // 调用bind和listen监听在一个套接字上
    if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) {
        error(1,errno,"bind failed");
    }

    if (listen(listenfd,5) < 0) {
        error(1,errno,"listen failed");
    }

    clilen = sizeof(cliaddr);
    if ((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen)) < 0) {
        if (errno == EINTR)
            error(1,errno,"accept failed");
        else
            error(1,errno,"accept failed");
    }
	
    // 读取和发送数据
    char recv_buff[1024] = {0};
    while (true) {
        if (read(connfd,recv_buff,1024) == 0) {
            printf("client quit\n");
            break;
        }
        printf("receive: %s",recv_buff);
        char send_buff[1024] = {0};
        sprintf(send_buff,"Hello, %s",recv_buff);

        int nbytes = sizeof(send_buff);
        if (write(connfd,send_buff,nbytes) != nbytes)
            error(1,errno,"write error");
    }
	
    // 关闭套接字
    close(listenfd);
    close(connfd);

    return 0;
}

这段程序首先创建了一个套接字,将类型指定为AF_LOCAL(等价于AF_UNIX),并且使用字节流格式。之后设置sun_path创建一个本地文件路径标识的套接字上(必须是一个文件不能是一个目录),和普通的TCP服务端没有什么区别(将IP地址换成了文件路径)。在此之前调用了一个unlink,如果原先已经存在了相同名称的文件则将其删除。之后与TCP雷同,使用bind和listen来绑定套接字,使用accept来应答连接。之后就可以使用read和write来进行数据读写。

客户端

如下是客户端程序:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>

int main(int argc,char *argv[])
{
    if (argc != 2) {
        error(1,0,"usage: unixstreamclient <local path>");
    }

    int sockfd;
    struct sockaddr_un servaddr = {0};
    sockfd = socket(AF_LOCAL,SOCK_STREAM,0);
    if (sockfd < 0) {
        error(1,errno,"create socket failed");
    }
    
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path,argv[1]);

    if (connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) {
        error(1,errno,"connect failed");
    }

    char send_buff[1024] = {0};
    char recv_buff[1024] = {0};

    while (fgets(send_buff,1024,stdin) != NULL) {
        int nbytes = sizeof(send_buff);
        if (write(sockfd,send_buff,1024) != nbytes) {
            error(1,errno,"write error");
        }
        if (read(sockfd,recv_buff,1024) == 0) {
            error(1,errno,"server terminated prematurely");
        }
        
        printf("server: ");
        fputs(send_buff,stdout);
    }

    return 0;
}

首先创建一个套接字,使用字节流类型。之后设置目标服务器地址,即文件路径,随后使用connect进行连接(不会出现3次握手),最后使用read和write进行数据读写

运行测试

服务端:

$ sudo ./local_server /var/lib/unixstream.sock
receive: a
receive: b

客户端:

$ sudo ./local_client /var/lib/unixstream.sock
a
a
b
b

从客户端输入字符,服务端返回相同的字符

本地数据报套接字

在本地套接字上使用数据报

服务端

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>

int main(int argc,char *argv[])
{
    if (argc != 2) {
        error(1,0,"usage: unixdataserver <local path>");    
    }

    int socket_fd;
    socket_fd = socket(AF_LOCAL,SOCK_DGRAM,0);
    if (socket_fd < 0) {
        error(1,errno,"socket create failed");
    }

    struct sockaddr_un servaddr = {0};
    char *local_path = argv[1];
    unlink(local_path);
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path,local_path);

    if (bind(socket_fd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) {
        error(1,errno,"bind failed");
    }

    char recv_buff[1024] = {0};
    struct sockaddr_un clnt_addr = {0};
    socklen_t len = sizeof(clnt_addr);

    while (true) {
        if (recvfrom(socket_fd,recv_buff,1024,0,(struct sockaddr*)&clnt_addr,&len) == 0) {
            printf("client quit\n");
            break;
        }
        printf("receive: %s\n",recv_buff);

        char send_buff[1024] = {0};
        sprintf(send_buff,"%s",recv_buff);

        size_t nbytes = strlen(send_buff);
        printf("now sending: %s\n",send_buff);

        if (sendto(socket_fd,send_buff,nbytes,0,(struct sockaddr*)&clnt_addr,len) != nbytes) {
            error(1,errno,"sento error");
        }
    }

    close(socket_fd);

    return 0;
}

使用数据报套接字就无须再使用listen和bind,同时要使用recvfrom和sento来进行数据收发

客户端

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>

int main(int argc,char *argv[])
{
    if (argc != 2) {
        error(1,0,"usage: unixdataclient <local path>");
    }

    int sockfd;
    struct sockaddr_un clnt_addr = {0},serv_addr = {0};

    sockfd = socket(AF_LOCAL,SOCK_DGRAM,0);
    if (sockfd < 0) {
        error(1,errno,"create socket failed");
    }

    clnt_addr.sun_family = AF_LOCAL;
    strcpy(clnt_addr.sun_path,tmpnam(NULL));

    if (bind(sockfd,(struct sockaddr*)&clnt_addr,sizeof(clnt_addr)) < 0) {
        error(1,errno,"bind failed");
    }

    serv_addr.sun_family = AF_LOCAL;
    strcpy(serv_addr.sun_path,argv[1]);

    char send_buff[1024] = {0};
    char recv_buff[1024] = {0};

    while (fgets(send_buff,1024,stdin) != NULL) {
        int i = strlen(send_buff);
        if (send_buff[i-1] == '\n') {
            send_buff[i-1] = 0;
        }
        size_t nbytes = strlen(send_buff);
        printf("now sending %s\n",send_buff);

        if (sendto(sockfd,send_buff,nbytes,0,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) != nbytes) {
            error(1,errno,"sendto error");
        }

        int n = recvfrom(sockfd,recv_buff,1024,0,NULL,NULL);
        recv_buff[n] = 0;

        fputs(recv_buff,stdout);
        fputs("\n",stdout);
    }

    return 0;
}

这段代码和UDP套接字编程相似,但有一点较为不同,这里要将本地套接字bind到本地一个路径上,因为要指定一个本地路径,以便在服务端回包时,可以正确找到地址

运行测试

服务端

$ sudo ./local_dgram_server /tmp/unixdata.sock
receive: Hello!
now sending: Hello!

客户端

$ sudo ./local_dgram_client /tmp/unixdata.sock
Hello!
now sending Hello!
Hello!

posted @ 2022-07-16 17:01  N3ptune  阅读(543)  评论(0编辑  收藏  举报