【Linux】网络编程套接字


前言

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址. 通过目的IP地址可以确定网络上的一台主机.
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理.
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程.
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定.

一、相关接口介绍

1.socket

头文件:
#include <sys/types.h>
#include <sys/socket.h>

函数声明:
int socket(int domain, int type, int protocol);

参数说明:

  1. domain: Socket域, 指明通信方式.
    常用参数:
    AF_INET:用于IPv4网络通信。
    AF_INET6:用于IPv6网络通信。
    AF_UNIX:用于同一台机器上的进程间通信。
  2. type: Socket类型.
    常用参数:
    流式套接字(SOCK_STREAM):提供面向连接的、可靠的字节流服务,通常使用TCP协议。
    数据报套接字(SOCK_DGRAM):提供无连接的、不可靠的消息传递服务,通常使用UDP协议。
  3. protocol: 一般设置为0, 表示让系统自动选择与指定的域(domain)和类型(type)匹配的默认协议.

返回值:
调用成功返回一个非负整数,这个整数是一个套接字描述符。套接字描述符是一个文件描述符,可以用来标识和操作这个套接字. 调用失败返回 -1.

功能:
提供了一种用于进程间通信(IPC)的机制,可以在同一台机器或通过网络在不同的机器之间进行数据交换。套接字是网络通信的基本单元,广泛用于客户端-服务器模型中。

2.bind

头文件:
#include <sys/types.h>
#include <sys/socket.h>

函数声明:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  1. sockfd: 通过socket获得的返回值.
  2. addr: 一个指向struct sockaddr结构的指针,包含要绑定的地址信息。
  3. addrlen: 地址结构的长度(以字节为单位)。

返回值:
成功:返回0。
失败:返回 -1,并设置全局变量errno以指示错误类型。常见的错误包括:

  1. EADDRINUSE:指定的地址已被使用。
  2. EINVAL:套接字已绑定到一个地址。
  3. ENOTSOCK:sockfd不是一个套接字。

功能:
用于将套接字与特定的本地地址(包括IP地址和端口号)绑定, 这个操作通常在服务器端套接字上执行.

补充知识:
结构体struct sockaddr, 用于在网络编程中表示各种协议的地址, 通常不会直接使用, 而是通过其子类型来使用, 常用三种子类型:

  1. struct sockaddr_in (IPv4 地址)
  2. struct sockaddr_in6 (IPv6 地址)
  3. struct sockaddr_un

前两种用于网络通信, 最后一种用于同一台机器上进程间的通信, 而不涉及网络通信.

一般通常使用子类型struct sockaddr_in, 介绍其常用成员:

  1. sin_family: 地址族,用于指定地址的类型,例如: AF_INET, AF_INET6, AF_UNIX, 和socket中的参数domain作用一致.
  2. sin_addr: ip地址, 注意在网络通信时进行本地序列转网络序列.
  3. sin_port: 端口号, 注意在网络通信时进行本地序列转网络序列.

3.recvfrom

头文件:
#include <sys/types.h>
#include <sys/socket.h>

函数声明:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  1. sockfd:套接字文件描述符,标识一个已创建的套接字。
  2. buf:指向存储接收到数据的缓冲区的指针。
  3. len:缓冲区的长度,即可以接收的最大字节数。
  4. flags:标志参数,一般设置为0。
  5. src_addr:指向 sockaddr 结构的指针,用于存储发送方的地址信息。可以为 NULL,表示不需要获取发送方的地址信息。
  6. addrlen:指向 socklen_t 变量的指针,用于存储 src_addr 所指向的地址结构的长度。如果 src_addr 为 NULL,则 addrlen 也应为 NULL。

返回值:
成功:返回接收到的字节数。
失败:返回 -1,并设置全局变量 errno 以指示错误类型。

功能:
用于接收数据报(datagram)套接字上的数据,并可选地获取发送方的地址信息。通常用于UDP(无连接的通信)套接字,但也可以用于其他数据报协议。

4.sendto

头文件:
#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);

参数说明:

  1. sockfd:套接字文件描述符,标识一个已创建的套接字。
  2. buf:指向要发送的数据的缓冲区的指针。
  3. len:要发送的数据的长度,以字节为单位。
  4. flags:发送数据时的标志,一般设置为0。
  5. dest_addr:指向 sockaddr 结构的指针,包含目标地址信息。
  6. addrlen:目标地址结构的长度(以字节为单位)。

返回值:
成功:返回实际发送的字节数。
失败:返回-1,并设置全局变量 errno 以指示错误类型。

功能:
用于通过数据报(datagram)套接字发送数据,通常用于UDP协议。它允许指定目的地址,使得数据可以发送到特定的目标。

5.listen

头文件:
#include <sys/types.h>
#include <sys/socket.h>

函数声明:
int listen(int sockfd, int backlog);

参数说明:

  1. sockfd:套接字文件描述符,标识一个已创建并绑定(bind)到本地地址的套接字。
  2. backlog:指定内核应该为相应套接字排队的最大连接个数。在所有排队的连接请求都已满的情况下,新的连接请求将被拒绝。

返回值:
成功:返回0。
失败:返回-1,并设置全局变量 errno 以指示错误类型。

功能:
用于将一个套接字设置为被动监听模式,这意味着这个套接字将等待来自远程的连接请求。通常,这在服务器应用程序中用于准备接受客户端连接请求。

6.accept

头文件:
#include <sys/types.h>
#include <sys/socket.h>

函数声明:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明:

  1. sockfd:监听套接字的文件描述符,该套接字是通过socket()创建并且通过bind()绑定到一个地址,通过listen()进入监听状态的。
  2. addr:指向struct sockaddr结构体的指针,用于存储连接客户端的地址信息。可以传递NULL,表示不关心客户端地址。
  3. addrlen:指向socklen_t类型的整数指针,表示addr的大小。调用完成后,它会包含客户端地址的实际大小。如果 addr 为 NULL,则 addrlen 也应为 NULL。

返回值:
成功: 返回一个新的套接字文件描述符,用于与客户端进行通信。这个新的文件描述符是专用于这个连接的。
失败时: 返回-1,并设置errno来指示错误类型。

功能:
用于接受已经在监听的套接字(socket)上的连接请求。它主要用于服务器程序中,以处理客户端的连接请求。

7.connect

头文件:
#include <sys/types.h>
#include <sys/socket.h>

函数声明:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  1. sockfd:这是通过 socket 函数创建的套接字描述符。
  2. addr:一个指向 sockaddr 结构的指针,包含了服务器的IP地址和端口号。
  3. addrlen:addr 结构的大小。

返回值:
成功: 返回 0。
失败: 返回 -1,并设置 errno 来指示错误原因。

功能:
用于客户端和服务器端建立连接的系统调用, 将一个未连接的套接字连接到一个远程地址(IP地址和端口号),从而建立一个TCP或UDP连接。

二、建立Udp服务器/客户端大概步骤

1.服务器

  1. 通过socket获得套接字.
int sock = socket(AF_INET, SOCK_DGRAM, 0);
  1. 定义struct sockaddr_in结构体, 并填充服务器信息.
struct sockaddr_in server;
bzero(&server, sizeof(server)); // 用0对server进行初始化
server.sin_family = AF_INET;    // 通信模式:网络通信
server.sin_port = htons(port); // 端口号
//填充ip有多种方式,以下方式任选其一:
/*1.*/server.sin_addr.s_addr = inet_addr(ip.c_str());
/*2.*/server.sin_addr.s_addr = INADDR_ANY; // bind主机上的任意ip

注意该结构体会在网络上传输, 而现在用来填充的变量都是局部变量, 需要转换为网络序列, 需要转换的也就端口号(port)和ip, htons() 可以用来转换端口号, inet_addr() 会将ip地址转为点分十进制格式并且自动将其从本地序列转为网络序列, 当我们手动填充ip时用inet_addr(), 自动填充ip就用INADDR_ANY.

  1. 通过bind对socket获得的套接字和填充后的struct sockaddr_in结构体进行绑定.
bind(_sock, (struct sockaddr *)&server, sizeof(server))

注意第二个参数要求为const struct sockaddr *, 所以要进行强制转换后传参.

  1. bind成功后就可以通过套接字与客户端通信了, 下面模拟接收到客户端的数据和向客户端发送数据.
//1.接收数据
char response[1024] = {'\0'};// 接收请求数据的缓冲区
struct sockaddr_in client; // 客户端信息
socklen_t response_len = sizeof(client); // 里一定要写清楚,传入的缓冲区大小
bzero(&client, sizeof(client)); // 初始化,清空为0
recvfrom(sock, response, sizeof(response), 0, (struct sockaddr *)&client, &response_len); // 接收数据

通过套接字接收客户端的数据, 这里recvfrom()的倒数两个参数是应该要填的, 因为后续还要向客户端发送数据, 所以要保存向服务器发送数据的客户端的信息.

//2.发送数据
string msg;
getline(cin, msg);
sendto(sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&client, sizeof(client));

注意要给谁发消息后面的结构体就填谁的.

2.客户端

  1. 通过socket获得套接字.
int sock = socket(AF_INET, SOCK_DGRAM, 0);
  1. 用服务器信息填充struct sockaddr_in结构体.
// 填充服务器信息,用于向其通信
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); //获取到的服务器ip
server.sin_port = htons(server_port); //获取到的服务器端口号

客户端的ip地址填充不能使用INADDR_ANY, 必须明确指定服务器的ip地址.

  1. 下面模拟向服务器发送数据并且从服务器接收到数据.
//1.发送数据
string request;
getline(cin, request);
sendto(sock, request.c_str(), request.size(), 0, (struct sockaddr *)&server, sizeof(server));

//2.接收数据

```cpp
char response[1024] = {'\0'};
recvfrom(sock, response, sizeof(response), 0, nullptr, nullptr);

这里recvfrom()不关心是谁给我发的, 因为包是服务器, 前面已经存储了服务器的信息了, 所以不需要接收.

ps: 客户端不用bind也不应该自己bind, 由系统自动为客户端进行bind.

3.补充

bzero()函数

头文件: #include <strings.h>

函数声明:
void bzero(void *s, size_t n);

功能:
将从s开始的区域的前n个字节设置为零(包含“\0”的字节).

ps: 以上服务器和客户端的业务处理逻辑只做简单的字符串传输, 要进行其他业务处理逻辑请自定义实现.

三、建立Tdp服务器/客户端大概步骤

1.服务器

  1. 通过socket获得套接字.
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);

注意第二个参数与Udp服务器的区别.

  1. 定义struct sockaddr_in结构体, 并填充服务器信息.
// 填充结构体
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = INADDR_ANY;
  1. 通过bind对socket获得的套接字和填充后的struct sockaddr_in结构体进行绑定.
bind(listen_sock, (struct sockaddr *)&server, sizeof(server));
  1. 将套接字设置为被动监听模式.
listen(listen_sock, 32);
  1. 接受已经在监听的套接字上的连接请求.
// 请求连接的客户端信息
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
bzero(&client, sizeof(client));

// accept
int sock = accept(_listen_sock, (struct sockaddr *)&client, &client_len);

一般accept()写在一个死循环体内, 时刻保持接收连接状态.

  1. accept成功后就可以通过accept返回的套接字与服务端通信了, 下面模拟接收到客户端的数据和向客户端发送数据.
//1.接收数据
// 接收缓冲区
char buf[1024] = {'\0'};
int n = read(sock, buf, sizeof(buf));
if(n > 0)
{
	//...
}
else if(n == 0)
{
	//...
}
else
{
	//...
}

因为Tcp是提供面向连接的、可靠的字节流服务, 注意字节流, 那当然可以用read来读取数据, 根据read的返回值不同来做出不同处理, 返回值大于0表示读取数据成功, 等于0表示客户端退出了, 小于0表示读取数据失败, 按需自定义处理方式.

//2.发送数据
string res;
getline(cin, res);
write(sock, res.c_str(), res.size());

2.客户端

  1. 通过socket获得套接字.
int sock = socket(AF_INET, SOCK_STREAM, 0);

注意第二个参数与Udp客户端的区别.

  1. 用服务器信息填充struct sockaddr_in结构体.
// 填充服务端信息
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip); //ip填获取到的服务器ip
server.sin_port = htons(port); //port填获取到的服务器端口号
  1. 使用connect将通过socket获得的套接字连接到一个服务器.
connect(sock, (struct sockaddr *)&server, sizeof(server));
  1. connect成功后可以与服务器进程通信, 下面模拟向服务器发送数据并且从服务器接收到数据.
//1.发送数据
string msg;
getline(cin, msg);
write(sock, msg.c_str(), msg.size());

//2.接收数据
// 接收缓冲区
char buff[1024] = {'\0'};
int n = read(sock, buff, sizeof(buff) - 1);
if(n > 0)
{
	//...
}
else if(n == 0)
{
	//...
}
else
{
	//...
}

四、服务器守护进程化

用Linux中打开的一个窗口就是一个会话, 会话中包含了一个个任务, 而一个任务可能由多个进程协力执行, 总之我们跑起来的服务器就是一个任务, 而当我们当前会话关闭时, 里面的所有任务都会关闭, 包括我们的服务器, 这不一定是我们想要的, 我们的预期应该是服务器一直在跑, 不受当前会话关闭与否的影响, 那么此时将服务器守护进程化, 即让其单独为一个会话, 这样就不会被干扰了.

通过函数setsid()可以使服务器守护进程化, 下面来简单认识一下该函数:
头文件:
#include <unistd.h>

函数声明:
pid_t setsid(void);

返回值:
成功: 返回新会话的ID(与调用进程的PID相同)。
失败: 返回 -1,并设置 errno 来指示错误原因。

实例代码:

// 1. 忽略某些信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);

// 2.不能让一个会话的话首进程来守护进程化
if (fork() > 0)
{
    return;
}

// 走到这是子进程
setsid();

//文件描述符重定向 -- 处理服务器中IO问题
int fd = open("/dev/null", O_RDWR); //文件/dev/null,向其写入的数据直接丢弃,从其中读取不到数据
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);

注意调用setsid()的进程不能是一个会话的话首进程, 否则setsid()会失败.

补充:
查看某进程是否是一个会话的话首进程的指令:

ps -o pid,sid,cmd -p 进程pid

运行结果:
在这里插入图片描述
其中SID就表示一个会话的话首进程的pid.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值