Socket编程
Socket编程是一种在网络中不同计算机之间实现数据交换的编程方式。它允许程序创建网络连接,并通过这些连接来发送和接收数据。Socket编程是网络编程的基础,广泛应用于客户端-服务器(C/S)架构中。
要实现双方通信,就需要对应的地址,那如何找到对方的网络地址呢
IP地址和端口号
- IP地址用于标识网络中的每一台设备
- 端口号用于标识设备上的特定应用程序或服务(进程)
- 端口号是一个 2 字节 16 位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来
处理;- 一个端口号只能被一个进程占用
- 端口号是传输层协议的内容
- 0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的
端口号都是固定的.- 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作
系统从这个范围分配的.
所以在网络通信中,可以通过IP地址锁定对应的主机,再通过端口号锁定对应主机上的应用程序或服务;
IP地址+端口号也成唯一标识的网络地址;
Socket也就是将IP和port套在一起,完成网络间的通信;
进程ID和端口号
- 进程ID是操作系统分配给每个正在运行的进程的唯一标识符。通过PID,操作系统可以跟踪和管理每个进程。
- 端口号是一个逻辑概念,用于区分同一台计算机上不同应用程序或服务所使用的网络通信端口。每个端口号都对应着一个特定的服务或应用程序的实例。
- 区别:进程ID用于操作系统内部管理和区分不同的进程,而端口号则用于网络通信中区分不同的应用程序或服务。
- 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
Socket的文件描述符
套接字(Socket)是一种在计算机网络中进行数据交换的端点。在UNIX和类UNIX系统中(如Linux和macOS),套接字被实现为文件描述符,这使得套接字可以像文件一样被打开、读写和关闭。文件描述符是一个非负整数,它是一个索引值,指向内核中每个进程打开文件的记录表。对于套接字而言,这个"文件"实际上是一个网络通信的端点。
网络字节序
网络字节序,也称为大端字节序(Big-Endian),是指在网络通信协议中规定的字节序 。
它规定整数的高位字节必须先传输到网络上,即高位字节在前,低位字节在后。
主机字节序是指特定主机或处理器所采用的字节序。它取决于主机的硬件架构,可能是大端序或小端序(Little-Endian)。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
cpp
#include <arpa/inet.h>
uint32_t htonl(uint32 t hostlong);
uint16_t htons(uintl6 t hostshort);
uint32_t ntohl(uint32tnetlong);
uint16_t ntohs(uintl6 ttnetshort);
Socket编程常见API
Socket是一个网络编程接口,是TCP/IP网络的API,它定义了一种方式,使应用程序可以通过网络与其他应用程序进行通信。
socket()
cpp
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket函数是用于创建一个新的套接字的系统调用。套接字(Socket)是网络通信的基本单元,它提供了一种在应用程序之间(可能位于同一台机器或不同的机器上)进行通信的方式;
参数说明:
- domain :指定协议族。常见的协议族有
AF_INET
(IPv4)、AF_INET6
(IPv6)、AF_UNIX
(本地套接字,用于同一机器上的进程间通信)等。 - type :指定套接字的类型。常见的套接字类型有
SOCK_STREAM
(流套接字,提供可靠的、面向连接的通信,如TCP)、SOCK_DGRAM
(数据报套接字,提供不可靠的、无连接的通信,如UDP)、SOCK_RAW
(原始套接字,允许应用程序直接操作网络层协议)等。 - protocol:指定协议。在大多数情况下,该参数可以设置为0,以选择协议族中默认的协议。
返回值:
- 如果成功,socket函数返回一个非负整数,即新创建的套接字的文件描述符。
- 如果失败,返回-1,并设置全局变量errno以指示错误。
bind()
cpp
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind函数通常与网络编程相关,特别是在使用套接字(sockets)进行通信时,
bind函数用于将套接字与特定的本地地址和端口号绑定,从而确定套接字在网络上的标识。
参数说明:
- sockfd:套接字描述符,通常是通过socket函数创建的。
- addr :指向
struct sockaddr
结构体的指针,该结构体中包含了要绑定的地址和端口信息。通常,对于IPv4地址,会使用struct sockaddr_in
,而对于IPv6地址,则使用struct sockaddr_in6。这些结构体都需要强制类型转换为struct sockaddr*
来传递给bind函数。 - addrlen:addr结构体的大小。
返回值:
如果成功,bind函数返回0。
如果失败,返回-1,并设置全局变量errno以指示错误。
sockaddr_in结构体
cpp
struct in_addr
{
uint32_t s_addr; /* IPv4 地址 */
};
struct sockaddr_in
{
shortsin_family;/*地址家族,AF_INET */
unsigned shortsin_port;/*端口号,网络字节序*/
struct in_addr sin_addr;/* IPv4 地址*/
char sin_zero[8];/*未使用的字节,用零填充以保持结构大小与sockaddr相同3;
};
sockaddr_in是用于IPv4地址的套接字地址结构 。它包含在sys/socket.h
头文件中,并且是sockaddr结构的特定于IPv4的版本。当你需要在网络编程中处理IPv4地址时,你会经常与这个结构打交道。
成员变量:
sin_family
:这是一个地址家族标识符,对于IPv4 来说,它通常被设置为AF_INET
。sin_port
:这是一个 16 位的端口号,它使用网络字节序(大端字节序)。在将端口号赋值给sin_port
之前,你需要使用htons()
函数(host to networkshort)来将主机字节序转换为网络字节序。sin_addr
:这是一个in_addr
结构,包含一个 32 位的IPv4 地址。在赋值之前,你可能需要将点分十进制表示法(如"192.168.1.1")转换为 32 位整数。这通常通过inet _pton()
函数(presentation to network)来完成。sin_zero
:这是一个填充字段,用于确保sockaddr_in
结构的大小与通用的 sockaddr 结构相同。在实际使用中,这个字段通常被忽略。
recvfrom()
在使用套接字(sockets)进行UDP通信时,这个函数用于从指定的套接字接收数据,并返回发送数据的地址 。
与TCP使用的 recv() 函数不同,recvfrom() 特别适用于无连接的UDP协议,因为它需要知道数据是从哪个地址发送来的。
cpp
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
- sockfd:套接字文件描述符,标识了一个打开的套接字。
- buf:指向一个缓冲区的指针,用于存放接收到的数据。
- len:指定了buf的大小,即可以接收的最大字节数。
- flags:通常设置为0,但在某些特殊情况下可以用来控制函数的行为(如MSG_DONTWAIT标志,用于非阻塞套接字)。
- src_addr:一个指向sockaddr结构的指针,该结构在调用后会被填充为发送数据的源地址。如果调用者不关心源地址,这个参数可以设置为NULL。
- addrlen:是一个指向socklen_t的指针,在调用前应该包含src_addr指向的sockaddr结构的大小,在调用后,这个值会被更新为实际使用的结构大小。
返回值
- 成功时,recvfrom() 返回接收到的字节数。
- 如果连接被对方正常关闭,则返回0。
- 如果发生错误,则返回-1,并设置相应的errno以指示错误类型
sendto()
用于UDP协议,允许开发者将数据从指定的套接字发送到对方主机,同时指定目的地址
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明
- sockfd:套接字文件描述符,标识了一个打开的套接字。
- buf:指向要发送数据的缓冲区的指针。
- len:要发送的数据的字节数。
- flags:指定发送数据的方式,通常设置为0即可。但可以改变flags来改变sendto()发送的形式。
- dest_addr:一个指向目的地址结构体的指针,包括目的IP地址和端口号等信息。
- addrlen:表示目的地址结构体的长度。
返回值
- 成功时,sendto() 返回实际发送的字节数。
- 如果发生错误,则返回-1,并设置相应的errno以指示错误类型。
recv()
recv() 函数是网络编程中用于接收数据的函数,它主要用于TCP协议;
与recvfrom()不同,recv()不需要指定数据的来源地址,因为TCP是面向连接的协议,每个套接字(socket)都直接关联到一个特定的连接,所以接收的数据自然就是来自该连接的另一端。
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明
- sockfd:套接字文件描述符,标识了一个打开的套接字。
- buf:指向一个缓冲区的指针,用于存放接收到的数据。
- len:指定了buf的大小,即可以接收的最大字节数。
- flags:指定接收数据的方式,通常设置为0即可。但可以改变flags来改变recv()的行为,例如使用
MSG_DONTWAIT
标志进行非阻塞接收。
返回值
- 成功时,recv() 返回接收到的字节数。
- 如果连接被对方正常关闭,并且已经接收到了对方发送的FIN包,则返回0。
- 如果发生错误,则返回-1,并设置相应的errno以指示错误类型。
send()
send() 函数是网络编程中用于发送数据的函数,它主要用于TCP协议。
与sendto()不同,send()不需要指定目的地址,因为TCP是面向连接的协议,每个套接字(socket)都直接关联到一个特定的连接,所以发送的数据自然就是发送到该连接的另一端。
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明
- sockfd:套接字文件描述符,标识了一个打开的套接字。
- buf:指向要发送数据的缓冲区的指针。
- len:要发送的数据的字节数。
- flags:指定发送数据的方式,通常设置为0即可。但可以改变flags来改变send()的行为,例如使用
MSG_DONTWAIT
标志进行非阻塞发送。
返回值
- 成功时,send() 返回实际发送的字节数。这可能与请求发送的字节数相同,也可能不同(特别是在非阻塞模式下)。
- 如果发生错误,则返回-1,并设置相应的errno以指示错误类型。
listen()
listen() 函数是在网络编程将套接字(socket)设置为监听状态 ,等待客户端的连接请求。常用于TCP协议。
cpp
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数说明
- sockfd:套接字文件描述符,这是之前通过 socket() 函数创建的套接字,并且已经通过 bind() 函数绑定了特定的IP地址和端口号。
- backlog:这个参数指定了操作系统可以挂起(即等待处理)的连接请求的最大数量。当超过这个数量时,新的连接请求可能会被拒绝。需要注意的是,这个值并不直接对应于队列中的最大连接数,而是操作系统用来处理并发连接请求的一个参数。
返回值
- 监听成功时返回0
- 监听失败时返回-1
函数作用
- 设置监听状态:listen() 函数将 sockfd 指定的套接字设置为监听状态,使其能够接收来自客户端的连接请求。
- 维护请求队列:操作系统会为处于监听状态的套接字维护两个队列:
未完成连接队列(SYN_RCVD
):这个队列用于存放那些已经接收到客户端的SYN包,但尚未完成三次握手过程的连接请求。 - 已完成连接队列(
ESTABLISHED
):当三次握手过程完成后,连接请求会被移动到这个队列中,等待服务器端的accept()
函数来接受。 - 控制并发连接:通过 backlog 参数,listen() 函数帮助服务器控制可以同时处理的并发连接数量,从而避免系统资源的过度消耗。
accept()
accept() 函数: 接受一个客户端的连接请求,并返回一个新的套接字(socket)描述符,用于与客户端进行后续的通信。常用于TCP协议。
cpp
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明
- sockfd:这是一个套接字描述符,用于指定服务器端的监听套接字。这个套接字是之前通过- socket()函数创建的,并且已经通过bind()函数绑定了特定的IP地址和端口号,还通过listen()函数设置为监听状态。
- addr:这是一个指向sockaddr结构的指针,用于存储连接请求的客户端的地址信息。如果调用者不关心客户端的地址信息,可以将此参数设置为NULL。但通常,为了后续与客户端的通信,我们需要知道客户端的地址信息。
- addrlen:这是一个指向socklen_t变量的指针,该变量在调用前应该被设置为addr所指向的sockaddr结构的大小。在调用accept()后,该变量会被更新为实际存储在addr中的地址信息的大小。
返回值
- 成功时,accept()函数返回一个非负的整数,即新创建的套接字描述符。
- 失败时,返回-1,并设置errno以指示错误原因。
函数作用
- 接受连接请求:当服务器端的监听套接字收到一个客户端的连接请求时,accept()函数会被调用以接受这个请求。如果监听套接字当前没有连接请求,accept()函数会根据套接字的阻塞/非阻塞属性来决定是立即返回错误还是等待直到有连接请求到来。
- 创建新套接字:一旦accept()函数接受了一个连接请求,它就会为这次连接创建一个新的套接字描述符,并返回这个描述符给调用者。这个新的套接字描述符用于后续的通信操作,如发送和接收数据。
- 返回客户端信息(如果addr不为NULL):accept()函数还会将连接请求的客户端地址信息存储在addr所指向的sockaddr结构中,并通过addrlen参数返回实际存储的字节数。这样,服务器就可以知道是哪个客户端发起的连接请求。
connect()
connect() 函数用于客户端程序,以建立与服务器之间的连接。常用于TCP协议。
cpp
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
- sockfd:这是一个套接字描述符,用于指定要发起连接的客户端套接字。这个套接字是之前通过socket()函数创建的。
- addr:这是一个指向sockaddr结构的指针,包含了目标服务器的地址信息,包括IP地址和端口号。在实际编程中,由于sockaddr是一个通用的地址结构,通常会使用其特定类型(如sockaddr_in对于IPv4)来方便地设置地址和端口。
- addrlen:这个参数指定了addr所指向的地址结构的大小,通常是通过sizeof()运算符来获取的,如sizeof(struct sockaddr_in)。
返回值
- 如果连接成功,函数返回0;
- 如果连接失败,则返回-1,并设置全局变量errno以指示错误原因。