Linux下的Socket编程

发布于:2025-05-23 ⋅ 阅读:(19) ⋅ 点赞:(0)

1 网络编程基础

1.1 IP地址

        网络通信前,通信双方需要知道本机与对方主机的IP地址,方可进行通信。在IP数据报的报头中需要包含有源IP地址目的IP地址这两个地址,源IP地址即为本机地址,目的IP地址即为对方地址。

        其中,在报头中包含的目的IP地址用于本机向对方主机发送请求报文,而源IP地址则是使对方主机能知道该报文的发送方身份。当对方主机将报文处理完毕后,会将请求报文的源IP地址作为响应报文的目的IP地址,从而对不同主机的请求进行正确响应。

1.2 端口号

        网络通信的本质是进程间通信,而通过IP地址只能实现两主机之间的网络通信,但并不能找到两个主机上进行通信的进程。因此,传输层引入端口号用于标识两个主机上通信的进程。

        端口号通常为16位无符号整数,取值范围为 0~65535,每个端口号只能绑定一个进程以用于标识该进程,这个过程是在程序代码中固定。和IP地址一样,在传输层协议报头中需要包含源端口号目的端口号,以便向正确的进程进行请求和响应。

        有了端口号,我们就可以找到主机上的唯一进程,结合IP地址,即可确定主机和进程。通常采用 “ IP地址:端口号 ” 的格式标识特定主机的特定进程,如 “ 192.168.0.1:8265 ”。

         事实上,每一个进程都有自己的 pid ,那么为什么还要引入端口号用于标识进程呢?

        进程 pid通常依赖操作系统的实现,兼容性较差,如果在网络通信中采用 pid 来标识,就必须要求通信双方的操作系统对进程 pid 的管理分配方式一致,目前这显然已经很难实现了。

        而端口号位于协议层面,要求通信双方遵守协议,从而实现不同系统之间的通信兼容。另外使用端口号标识网络通信进程同时使网络通信与操作系统解耦合。

1.3 网络字节序

        目前主流的字节序主要包括大端字节序小端字节序两种。不同厂家生产的机器可能采用不同的字节序。

        而不同字节序的机器在内存中存储数据的方式是不一样的,例如,大端和小端机器在存储32位整数时有如下区别。

数值:0x3584A02C

大端字节序
35 84 A0 2C
低地址 -> 高地址

小端字节序
2C A0 84 35
低地址 -> 高地址

        由于在网络通信中,数据通常以字节流的形式传递。由于字节序的不同,导致经过网络传输后,接收端解析后得到的与源数据不同,如上例中,使用大端机向小端机直接发送 “ 0x3584A02C ”,解析后的结果将为 “ 0x2CA08435 ”。

        为了避免这种情况发生,TCP/IP协议规定,网络通信字节序统一为大端字节序,而对于小端机器,进行网络通信时,需自行进行字节序转化。关于机器字节序和网络字节序之间的转换,C语言提供了相应的库函数支持。

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

        从接口名称可以分辨出各个接口的作用,以 “ htonl ” 为例,h 表示 host ,n 表示 network,l 表示 32 位,s 表示 16 位,所以 “ htonl ” 表示将32位数据从主机字节序转为网络字节序。

2 Socket编程

        Socket(套接字)也是一种进程间通信方式,可用于网络通信,主要基于传输层协议实现,如TCP和UDP协议。

        这里先简单介绍 Socket 编程中常用的接口。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

        该接口用于创建套接字,其返回值为该套接字的文件描述符,若创建失败则返回 -1。参数 domain 表示协议族,目前网络通信中通常使用 “ AF_INET ”,即 IPv4 协议;type 为套接字类型,网络通信中通常使用 “ SOCK_DGRAM ”,即数据报类型(UDP协议用),或 “ SOCK_STREAM ” 字节流类型(TCP协议用)。protocol 为协议编号,通常为 0 表示默认。

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

struct sockaddr_in {
           sa_family_t     sin_family;     /* AF_INET */
           in_port_t       sin_port;       /* Port number */
           struct in_addr  sin_addr;       /* IPv4 address */
};

        该接口用于将套接字与主机信息绑定。其参数 sockfd 为套接字文件描述符;addr 为主机信息,在进行网络通信是,通常传入结构 “ sockaddr_in ” 的对象指针,该结构的三个成员变量分别为协议族、端口号、地址,若使用 IPv4 协议进行通信,则将协议族设置为 “ AF_INET ”即可;addrlen 表示 addr 对象的大小。

        当一个套接字描述符与一个主机绑定后,对该描述符的读写操作即可认为是向该主机的接收和发送操作。

#include <sys/socket.h>

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

        以上三个接口主要用于TCP协议建立连接。

        listen 接口用于将套接字设置为监听状态(等待连接),通常用于服务端。参数 sockfd 为被设置的套接字描述符;backlog 为该套接字所允许建立的最大连接数。

        当服务端被设为监听状态后,客户端即可使用 connet 接口对服务端发起三次握手,以建立连接。其参数效果与 bind 类似,用于将给定套接字描述符与给定主机信息(即被连接的主机)建立连接。

        accept 接口的作用是当服务端开始监听后,阻塞进程,直到被监听的套接字获取到新连接。参数 addr 和 addrlen 用于获取新连接的主机信息。成功获取连接后,该接口返回新连接的套接字描述符。

#include <sys/socket.h>

// TCP 发送(需先建立连接)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

// UDP 发送(指定目标地址)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

// TCP 接收
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

// UDP 接收(获取发送方地址)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

        以上四个接口用于网络通信中数据的发送和接收,其本质上是对文件进行读写。

        以 sendto 为例,其前三个参数与 write 接口类似,最后两个参数用于指定目标主机和进程,flags 通常设置为 0 表示默认,或设置为 “ MSG_DONTWAIT ” 表示非阻塞。