文章目录
socket
Socket(套接字)是计算机网络编程中的一个抽象概念,用于在不同计算机之间进行通信。它是一种通信机制,允许计算机上的进程通过网络进行数据传输。Socket可以视为一种特殊的文件描述符,通过它可以进行读取和写入操作,就像操作文件一样。
socket的组成:IP + port
- IP负责找到网络里的唯一主机。
- port(端口号) 负责找到该主机上的唯一进程。
ip地址:标识网络里的唯一主机
记住2个特殊的ip地址:
0.0.0.0
:某个套接字绑定到 0.0.0.0 地址意味着该套接字将接受来自任何本地网络接口的连接。
127.0.0.1
: 是本地回环地址,通常被称为 “localhost”。这个地址通常用于在本地主机上进行网络通信。在网络编程中,使用 127.0.0.1 地址可以实现在同一台计算机上的不同进程之间进行通信。
端口号的特点
-
端口号和进程pid的联系与区别:
PID是操作系统为每个进程分配的唯一标识符,用于在系统中区分不同的进程;而端口号是用于在网络通信中标识不同应用程序或服务的数字标识符。二者对于进程都具有唯一性。 -
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
-
在计算机网络中,一些常见的端口号默认被特定的服务或进程占用。
端口 80:HTTP服务通常使用此端口来提供Web服务。
端口 443:HTTPS服务通常使用此端口来提供加密的Web服务。
端口 21:FTP(文件传输协议)服务器通常使用此端口来进行文件传输。
端口 22:SSH(安全外壳协议)服务器通常使用此端口来进行安全的远程访问。
端口 25:SMTP(简单邮件传输协议)服务器使用此端口来传输电子邮件。
……
预备知识
TCP和UDP 协议
在网络编程中,常用的协议(如TCP/IP、UDP等)都基于Socket来实现
下面简单介绍一下
TCP
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
简单的理解:
TCP相当于打电话,先要建立连接,保证传输安全,再进行通信。
UDP相当于发邮箱,不用先建立连接,直接发,至于数据是否丢失、出错等,UDP并不关心。如果出问题,直接重发。
UDP的不可靠并不是贬义,它仅仅是对UDP特点的描述。连接的可靠性是有代价的,需要维护可靠性,这意味TCP更复杂,而不可靠意味UDP很简单。根据不同的应用场景,选择合适的协议的。
网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
大端:低字节序放在高地址端,高字节序放在低地址端
小端:低字节序放在低地址端,高字节序放在高地址端
简记:小小小
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
socket接口及辅助接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
上述接口,会发现一个结构体sockaddr,这个结构体是什么?
sockaddr
sockaddr(socket address)结构体是用于表示套接字地址的数据结构。网络底层协议不同,套接字结构体也不同。
套接字编程的种类:
- 域间套接字:同一个机器通信
- 原始套接字:绕过传输层,直接访问网络层
- 网络套接字:网络通信
不同的结构体导致使用难度上升,于是设计者设计了一套统一的接口sockaddr。
sockaddr_in和sockaddr_un是具体我们使用的套接字结构体。当我们将它们的指针作为参数传给socket接口函数,强制转换为sockaddr*。
如下:
struct sockaddr_in local;
int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
在函数内部,函数会解析local的前2个字节,如果是AF_INET就表示使用IPv4的网络通信,如果AF_UNIX就表示使用本地通信。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
那为什么不像很多系统接口一样,使用void * 来作为参数类型呢?
因为那时候c语言还没有void*
下面介绍一下sockaddr_in
sockaddr_in 是在 Linux 中用于表示 IPv4 地址和端口的结构体
官方的定义如下:
简化一下:
struct sockaddr_in {
unsigned short sin_family; // 地址族 (AF_INET)
unsigned short sin_port; // 端口号
struct in_addr sin_addr; // IPv4 地址
char sin_zero[8]; // 填充 0 的字节以使结构与 sockaddr 结构的大小相同
};
- sin_family 字段表示地址族,对于 IPv4 地址来说,它的值通常是 AF_INET。
- sin_port 字段表示端口号,以网络字节序存储 (大端字节序)。
- sin_addr 是一个 struct in_addr 类型的结构体,用于存储 IPv4 地址。
- sin_zero 是用于填充的额外字段,以使 sockaddr_in 结构体的大小与 sockaddr 结构体的大小相同。
udpserver和udpclient
udpserver
std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8888;
class udpserver
{
public:
udpserver(uint16_t port = defaultport) :_port(port)
{
}
~udpserver()
{
close(_sockfd);
}
void init()
{
//初始化服务器
}
void run()
{
//运行服务器
}
private:
int _sockfd; //socket 文件描述符
std::string _ip = defaultip; //点分十进制的ip地址
uint16_t _port; //端口号
};
初始化udp服务器
void init()
{
// 1 create sockfd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2 init sockadd_in
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清0
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str()); /
// 3 bin socket
bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
}
- create sockfd
int socket(int domain, int type, int protocol);// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
参数:
- domain: 指定通信协议族。常见的包括 AF_INET(IPv4),AF_INET6(IPv6),AF_UNIX(Unix 域套接字),等等。
- type: 指定套接字的类型,常见的包括
SOCK_STREAM
(字节流式套接字,用于面向连接的通信,如 TCP),SOCK_DGRAM
(数据报套接字,用于无连接的通信,如 UDP),以及其他类型。- protocol: 指定协议。在大多数情况下,可以指定为 0,表示使用默认协议。对于某些特定的协议族和类型组合,需要指定具体的协议。
我们创建的是ipv4的udpserver, 因此使用如下:
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
- init sockadd_in
分别对3个成员进行初始化,注意细节:
- port:网络通信采用的是大端字节序,如果你使用的小端存储的机器,需要将port改为大端字节序。但你怎么知道自己使用的机器是大端还是小端?难道自己还有判断一下?有辅助接口可以解决这个麻烦。
- ip:其一、地址不是直接赋给sin_addr, sin_addr类型定义如下:
struct in_addr sin_addr;
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
sin_addr的类型是in_addr的结构体,地址保存在in_addr的成员s_addr中,使用时要注意。
其二、由于用户使用的是点分十进制风格的ip地址(字符串),因此有辅助接口帮助我们进行转换。
使用如下:
local.sin_addr.s_addr = inet_addr(_ip.c_str());
inet_aton(_ip.c_str(), &local.sin_addr);
- bind socket
现在有了socket结构体和sock文件描述符,我们还需要绑定二者。
bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
由此udpserver的核心工作就完成了,为什么?因为udp是无连接的,不用关心消息传输的问题。
运行udp服务器
udpserver简易收发消息代码:
void run()
{
char buffer[SIZE]; //缓存
while(_isRunning)
{
buffer[0] = 0;
struct sockaddr_in client;
socklen_t len = sizeof(client);
recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&client, &len);
buffer[n] = 0;
//处理服务器收到的数据报
std::string client_message = buffer;
client_message += " Server has received";
//发送数据报
sendto(_sockfd, client_message.c_str(), client_message.size(), 0, (struct sockaddr* )&client, len);
}
}
recvfrom和sendto是udp收发数据报的函数。
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd:指定待接收数据的套接字描述符。
- buf:指向用于存放接收数据的缓冲区。
- len:指定接收数据缓冲区的大小。
- flags:一组标志,通常设置为0。
- src_addr:指向用于存放发送端地址信息的结构体指针。
- addrlen:指向一个整数,用于指定 src_addr 结构体的大小。
- 返回接收到的字节数
sendto
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。
- dest_addr:指向包含目标地址信息的结构体指针。
- addrlen:指定 dest_addr 结构体的大小。
- 返回实际发送的字节数。
完整的服务器代码,这里的log.hpp是我写的一个简易日志。
#include <iostream>
#include <string>
#include <memory>
#include <unistd.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
extern logger log;
std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8080;
int size = 1024;
class udpserver
{
public:
udpserver(uint16_t port = defaultport) :<