Linux内核编程--网络协议与套接字编程

2022-05-09 21:44:17 浏览数 (1)

一,TCP/UDP协议

客户端和服务器在同一个以太网中的TCP/IP通信示意图:

传输层常用协议:TCP(可靠传输)/UDP(不可靠传输)

网络层常用协议:IPv4/IPv6

网络相关的shell指令:

查看网络状态:netstat

获得网络端口的详细信息:ifconfig

1.UDP协议简介:

UDP是面向无连接的协议。

UDP使用数据报套接字(Datagram Socket)进行通信,因为数据报有长度,所以传输的消息有记录边界。

应用进程发送的消息被封装到UDP数据报,UDP数据报被封装到IP数据报,最终的数据报被发送到目的地。

UDP缺乏可靠性,不能保证数据一定能送达,也不能保证数据被送达的频次和先后顺序。

具体流程:

客户端:socket()->bind()->sendto()->recvfrom()->close() //客户端可以不用bind()

服务器:socket()->bind()->recvfrom()->sendto()->close()

流程图:

2.TCP协议简介:

TCP是面向连接的协议,建立连接的过程有三次握手和四次握手。

TCP使用流套接字(Stream Socket)进行通信,因为流没有长度,所以传输的消息没有记录边界。

客户端使用TCP协议与服务器进行通信时,需要先建立连接,然后才能进行数据交换。

TCP提供了消息确认和重传机制,保证了传输的可靠性。

TCP提供了流量控制,流量控制的大小取决于接收缓冲区可用空间的大小。客户端发送一次数据,接收缓冲区可用空间变小。服务器接收一次数据,接收缓冲区可用空间变大。

TCP连接为全双工通信,而UDP既可以全双工通信,也可以使用别的通信模式。

TCP连接的建立:

通信的两种模式:SYN & ACK

SYN:用来发送新信号

ACK:用来返回确认信号

三次握手:

注意:握手时的“J”和“K”均不携带通信数据,主要包含TCP/IP的首部和一些TCP选项。

第一次握手:客户端通过SYN的方式,发送“J”信号给服务器,为了告诉服务器“客户端即将发送数据的初始序列号”。

第二次握手:服务器拿到“J”信号后,用ACK的方式把加1后的“J”信号返回给客户端,告诉客户端已经收到信息。

服务器还要以SYN的方式新发送一个“K”信号给客户端,为了告诉客户端“服务器即将发送数据的初始序列号”。

第三次握手:客户端拿到“K”信号后,用ACK的方式把加1后的“K”信号返回给服务器,告诉服务器已经收到信息。

四次握手:在三次握手的基础上增加了”关闭确认“环节,用得不多,此文篇幅有限不作介绍。

如果把建立TCP连接比作一次打电话:

socket:相当于双方通信的电话号码。

connect/accept:connect相当于呼叫人拨通了对方的电话(主动打开),accept相当于被呼叫人接到了呼叫人打来的电话(被动打开)。

listen:相当于在电话打来之前,被呼叫人一直守在电话前等着电话铃声响。

bind:相当于给座机配一个电话号码。

--举例方式参考《UNIX网络编程》

具体流程:

*三次握手和四次握手主要发生在connect/accept阶段。

客户端:socket()------------------->connect()->I/O操作->close()

服务器:socket()->bind()->listen()->accept()->I/O操作->close()

流程图:

二,TCP套接字编程

*由于套接字被当作一种文件描述符,所以有些处理文件描述符的函数(write()、read())也可以用来处理套接字。

1.和套接字地址信息有关的结构体

IPv4套接字地址结构体:sockaddr_in

代码语言:javascript复制
struct sockaddr_in {
    uint8_t         sin_len;
    sa_family_t     sin_family;
    u_short         sin_port;
    struct in_addr  sin_addr;
    char            sin_zero[8];
};
struct in_addr{
    unsigned long s_addr;     //load with inet_aton()‏
};

IPv6套接字地址结构体:sockaddr_in6

代码语言:javascript复制
struct sockaddr_in6 {
   uint8_t         sin6_len;
   sa_family_t     sin6_family;
   in_port_t       sin6_port;
   uint32_t        sin6_flowinfo;
   struct in6_addr sin6_addr;
   uint32_t        sin6_scope_id;
};
struct in6_addr
{
    unsigned long s6_addr;  
};

通用套接字地址结构体:sockaddr

代码语言:javascript复制
struct sockaddr{
    unsigned short sa_family;  //address family
    char sa_data[14];          //protocol address
};

2.常见函数

创建套接字--socket()

socket()的作用除了创建套接字,还初始化了套接字通信用到的套接字类型和协议类型(IPv4 TCP, IPv4 UDP)。调用socket()与调用open()类似,均可获得描述符。当不再使用描述符时,调用close()来关闭对文件或套接字的访问,释放文件描述符或套接字描述符。

代码语言:javascript复制
#include <sys/socket.h>
int socket(int domain, int type, int protocol)

--若成功,返回套接字描述符sockfd;若出错,返回-1

--domain参数:声明协议中的地址族(address family) AF_开头

address family

描述

AF_INET

IPv4

AF_INET6

IPv6

AF_UNIX/AF_LOCAL

UNIX本机

AF_UPSPEC

未指定

--type参数:声明套接字类型

type=SOCK_STREAM时,就像与对方打电话,需要双方建立通信链路,且对话中不包含对方的地址,两个通信进程之间需要建立逻辑连接。

type=SOCK_DGRAM时,就像发邮件一样,数据报中需要包含接收者的地址,多个发邮件任务之间相互独立,每封邮件还可以发送给不同的接收者,且发送顺序是无序的,有的信件还会在途中丢失,两个通信进程之间不需要建立逻辑连接。

类型

描述

SOCK_DGRAM

数据报套接字

SOCK_RAW

原始套接字

SOCK_SEQPACKET

有序分组套接字

SOCK_STREAM

字节流套接字

--protocol参数:选择所给定domain和type组合的系统默认值

protocol=0, 表示为给定的域和套接字类型选择默认的协议。

domain=AF_INET, type=SOCK_STREAM时,protocol=0对应的默认协议是TCP。

domain=AF_INET, type=SOCK_DGRAM时, protocol=0对应的默认协议是UDP。

protocol

说明

IPPROTO_TCP

TCP传输协议

IPPROTO_UDP

UDP传输协议

IPPROTO_SCTP

SCTP传输协议

关闭套接字--close()/shutdown()

除了用close()关闭套接字,要一次性关闭所有的套接字描述符的引用,或关闭套接字的单个方向,推荐使用shutdown()。

代码语言:javascript复制
#include <sys/socket.h>
int shutdown(int sockfd, int how)

how=SHUT_RD, 关闭读端
how=SHUT_WR, 关闭写端
how=SHUT_RDWR, 同时关闭读端和写端

--若成功,返回0。若出错,返回-1。

将套接字与地址关联--bind()

bind()操作把一个本地协议地址和一个套接字进行了绑定,为了方便客户端根据地址找到服务器的位置。

代码语言:javascript复制
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len)

--若成功,返回0。若出错,返回-1。

在进程正在运行的计算机上,指定的地址必须有效,不能指定其他机器的地址。

地址必须和创建套接字时的地址族所支持的格式相匹配。

地址中的端口号必须不小于1024,除非进程具有超级用户的特权。

一般只能将一个套接字端点绑定到一个给定的地址上,尽管有些协议允许多重绑定。

如果调用connect()或listen(),但没有将地址绑定到套接字上,系统会选一个默认地址去绑定。

套接字地址的获得:

a.可以调用getsockname()来发现绑定到套接字上的地址。

b.如果套接字已经和对方连接,可以调用getpeername()来找到对方的地址。

代码语言:javascript复制
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp)
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp)

--若成功,返回0。若出错,返回-1。

建立连接--connect()

如果要处理一个面向连接的网络服务(SOCK_STREAM或SOCK_SEQPACKET),在交换数据前,需要在客户端进程的套接字和服务端进程的套接字之间建立一个连接。如果用到的是TCP协议套接字,connect()会触发TCP的三次握手/四次握手,而且仅在连接建立成功或出错时才返回。

代码语言:javascript复制
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);

--若成功,返回0。若出错,返回-1。

监听套接字--listen()

仅在套接字协议为TCP时调用listen(),调用listen()将导致套接字从CLOSED状态变为LISTEN状态。

代码语言:javascript复制
#include <sys/socket.h>
int listen(int sockfd, int backlog)

--若成功,返回0。若出错,返回-1。

backlog参数提示了系统能接受的最大连接请求数量。

接收连接请求--accept()

如果没有连接请求,accept()会一直阻塞直到请求到来。如果sockfd处于非阻塞模式,accept会立即返回-1。

代码语言:javascript复制
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len)

--若成功,返回新的描述符。若出错,返回-1。

sockfd参数由socket()创建,随后用作bind()和listen()的第一个参数。

accept()成功执行后返回一个新的描述符,表示服务已连接。当服务器处理完客户端的请求时,该套接字会被关闭。

三,UDP套接字编程

发送数据--send()

代码语言:javascript复制
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags)
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)

若成功,返回发送的字节数;若出错,返回-1

send是面向连接的发送(必须先调用connect()进行连接),sendto可以在无连接的套接字上指定一个目标地址。

sendmsg可以指定多重缓冲区来发送数据。

代码语言:javascript复制
struct msghdr {
    void          *msg_name;        // protocol address
    socklen_t      msg_namelen;     // size of protocol address
    struct iovec  *msg_iov;         // scatter/gather array
    int            msg_iovlen;      // elements in msg_iov
    void          *msg_control;     // ancillary data (cmsghdr struct)
    socklen_t      msg_controllen;  // length of ancillary data
    int            msg_flags;       // flags returned by recvmsg()
};

接收数据--recv()

代码语言:javascript复制
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags)
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
                 struct sockaddr *restrict addr,
                 socklen_t *restrict addrlen
                 )
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags)
若成功,返回接收到的字节数;若无可用数据或发送已经结束,返回0;若出错,返回-1

recvfrom()通常用于无连接的套接字,在接收数据的同时,还可以定位发送者,获得发送者的源地址。

recvmsg()用于将接收的数据送入多个缓冲区,msghdr结构同sendmsg()。

四,常用的其他函数,了解即可

getsockopt/setsockopt:套接字校验和控制函数

gethostbyname/gethostbyaddr:主机名与IPv4地址之间转换

getservbyname/getservbyport:服务名与端口号之间转换

getaddrinfo/getnameinfo:主机名与IP地址之间转换

htons:将主机字节顺序转换成网络字节顺序

inet_aton:将点分十进制IP地址转换成网络字节序IP地址;

inet_pton:将点分十进制ip地址转化为用于网络传输的数值格式

inet_ntop:将网络传输用的数值格式转化为点分十进制的ip地址格式

代码样例:

服务器端:

代码语言:javascript复制
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>

int main(int argc, char *argv[])
{
    int sockfd, new_socket, server_len;
    struct sockaddr_in server;
    char *message;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1)
    {
        printf("Could not create socket");
    }

    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(8888);

    if(bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        puts("server bind failed");
        return 1;
    }
    puts("server bind done");

    listen(sockfd, 3);

    puts("Waiting for incoming connections...");

    server_len = sizeof(server);
    while( (new_socket = accept(sockfd, (struct sockaddr *)&server, (socklen_t*)&server_len)) )
    {
        puts("Connection accepted");
        message = "Hello Client, I have received your connection. But I have to go now, byen";
        write(new_socket, message, strlen(message));
    }

    if (new_socket<0)
    {
        perror("accept failed");
        return 1;
    }

    return 0;
}

客户端:

代码语言:javascript复制
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>

int main(int argc, char *argv[])
{
    int sockfd, new_socket;
    struct sockaddr_in client;
    char buffer[1024] = { 0 };

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    client.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &client.sin_addr);
    client.sin_port = htons(8888);

    if (connect(sockfd , (struct sockaddr *)&client , sizeof(client)) < 0)
    {
        puts("connect error");
        return 1;
    }
    puts("connect succeed");

    read(sockfd, buffer, 1024);
    printf("%sn", buffer);

    return 0;
}

运行结果:

服务器显示:

客户端显示:

*如果涉及到一个服务器处理来自多个客户端发来的请求,可以用fork创建一些子进程来处理客户端请求,在"pid == 0"的条件下完成处理。

参考教程:

《UNIX环境高级编程-第3版》

《UNIX网络编程 卷1:套接字联网API-第3版》

https://www.tutorialspoint.com/unix_sockets/socket_quick_guide.htm

https://www.binarytides.com/socket-programming-c-linux-tutorial/

0 人点赞