关于socket编程

socket在哪

image-20221005162447872

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议

TCP Socket编程

先看一张祖传图

基于 TCP 协议的客户端和服务器工作

两个结构体

网络中的地址包含3个方面的属性:
地址类型: ipv4还是ipv6 ip地址 端口

下面这两个结构体都是用来记录这些信息的

struct sockaddr

1
2
3
4
typedef struct sockaddr {
u_short sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /*14字节协议地址*/
} SOCKADDR, *PSOCKADDR, *LPSOCKADDR;
sa_family

是2字节的地址家族,一般都是“AF_xxx”的形式,它的值包括三种:AF_INETAF_INET6AF_UNSPEC

如果指定AF_INET,那么函数就不能返回任何IPV6相关的地址信息;如果仅指定了AF_INET6,则就不能返回任何IPV4地址信息。

AF_UNSPEC则意味着函数返回的是适用于指定主机名和服务名且适合任何协议族的地址。如果某个主机既有AAAA记录(IPV6)地址,同时又有A记录(IPV4)地址,那么AAAA记录将作为sockaddr_in6结构返回,而A记录则作为sockaddr_in结构返回

通常用的都是AF_INET

sa_data

包含套接字中的目标地址和端口信息

struct sockaddr_in

in代表Internet

由于在sockaddr 中, 我们把目标port和IP都放在一起了, 就显得很难受, sockaddr_in把他分开了

sin_port和 sin_addr 必须是网络字节顺序 (Network Byte Order)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
typedef struct sockaddr_in {
short sin_family; //在socket编程中只能是AF_INET
u_short sin_port; //存储端口号(使用网络字节顺序)
struct in_addr sin_addr; //存储IP地址,使用in_addr这个数据结构
char sin_zero[8]; //是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
} SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN;



/*
阐述下in_addr的含义,很显然它是一个存储ip地址的共用体, 有三种表达方式:

第一种用四个字节来表示IP地址的四个数字

第二种用两个双字节来表示IP地址

第三种用一个长整型来表示IP地址



*/
typedef struct in_addr {
union {
struct {
UCHAR s_b1;
UCHAR s_b2;
UCHAR s_b3;
UCHAR s_b4;
} S_un_b;
struct {
USHORT s_w1;
USHORT s_w2;
} S_un_w;
ULONG S_addr;
} S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

区别

sockaddr用14个字节来表示sa_data,而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。
sockaddr和sockaddr_in包含的数据都是一样的。

使用上的区别:

程序员不应操作sockaddr,sockaddr是给操作系统用的
程序员应使用sockaddr_in来表示地址,sockaddr_in区分了地址和端口,使用更方便。

也就是,程序员把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数

一些转换函数

本机转换

htons()–”Host to Network Short”

htonl()–”Host to Network Long”

ntohs()–”Network to Host Short”

ntohl()–”Network to Host Long”

为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢? 答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序

处理IP地址

给in_addr赋值的一种最简单方法是使用inet_addr函数,它可以把一个代表IP地址的字符串赋值转换为in_addr类型,如addrto.sin_addr.s_addr=inet_addr(“192.168.0.2”);
其反函数是inet_ntoa (network to ascii),可以把一个in_addr类型转换为一个字符串

需要注意的是inet_ntoa()将结构体in-addr作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的指针。它是一个由inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的IP地址, 你需要保存这个IP地址,使用strcopy()函数来指向你自己的字符 指针

创建Socket套接字

1
2
3
4
5
#include <sys/types.h>

#include <sys/socket.h>

int socket(int addressFamily, int socketType, int protocolType);

socketType

socketType

addressFamily 应该设置成 “AF_INET”然后

参数 type 告诉内核 是 SOCK_STREAM (TCP) 类型还是 SOCK_DGRAM (UDP) 类型

把 protocol 设置为 “0”

举个例子

1
2
3
4
5
6
7
int m_socket = socket(AF_INET, SOCK_STREAM, 0);	//IP4 TCP
if (m_socket == INVALID_SOCKET)
{
WSACleanup();
perror("socket()");
return 0;
}

服务端

bind()

1
2
3
4
5
int WSAAPI bind(
[in] SOCKET s, //标识未绑定套接字的描述符
[in] const sockaddr *name, //指向要分配给绑定套接字的本地地址的 sockaddr 结构的指针
[in] int namelen //名称参数指向的值的长度(以字节为单位)一般设置为sizeof(struct sockaddr)
);

如果未发生错误, 绑定将返回零。 否则,它返回SOCKET_ERROR,可以通过调用 WSAGetLastError 来检索特定的错误代码。

举个例子

1
2
3
4
5
6
7
8
9
10
11
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port); //选择绑定的端口,若为0, 则随机选择一个没有使用的窗口
addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//inet_addr("132.241.5.10") //使用自己的IP

if (bind(m_socket, (SOCKADDR*)&addr, sizeof(addr)) == SOCKET_ERROR)
{
perror("bind() ");
std::cout << "Error Code : " << WSAGetLastError() << std::endl;
return 0;
}

不要采用小于 1024的端口号。所有小于1024的端口号都被系统保留!你可以选择从1024 到65535的端口(如果它们没有被别的程序使用的话)

listen()

调用listen后, 一个套接字会从主动连接套接字变为一个监听套接字

1
2
3
4
int WSAAPI listen(
[in] SOCKET s, //标识绑定的未连接套接字的描述符
[in] int backlog
);
关于backlog参数
 SYN 队列 与 Accpet 队列

在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。

在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列

但是上限值是内核参数 somaxconn 的大小(大多数系统为20),也就说 accpet 队列长度 = min(backlog, somaxconn)

举个例子

1
2
3
4
5
6
7
// 建立监听队列,让套接字进入被动监听状态
if (listen(m_socket, MAX_LISTEN_NUM) == SOCKET_ERROR)
{
perror("listen() ");
std::cout << "Error Code : " << WSAGetLastError() << std::endl;
return 0;
}

accept()

socket 三次握手

首先,服务端创建welcomsocket并bind, listen后, 调用accept函数, 它是一个阻塞函数, 会阻塞直到客户端有请求到达

客户端向服务器发起连接时, 客户端建立一个TCP套接字, 指定了服务器中welcomsocket的地址, 即服务器主机的IP地址及其套接字的端口号

accept函数的返回值是一个新的套接字描述符,它代表的是和客户端的新的连接,可以把它理解成是一个客户端的socket,这个socket包含的是客户端的ip和port信息 。(当然这个new_socket会从welcomsocket中继承服务器的ip和port信息),而参数中的SOCKET s包含的是服务器的ip和port信息

1
2
3
4
5
SOCKET WSAAPI accept(
[in] SOCKET s, //一个描述符,用于标识已使用侦听函数处于侦听状态的套接字。 连接实际上是使用 接受返回的套接字建立的
[out] sockaddr *addr, //指向接收连接实体地址(称为通信层)的缓冲区的可选指针。 添加器参数的确切格式由创建 sockaddr 结构中的套接字时建立的地址系列确定
[in, out] int *addrlen //指向包含 addr 参数指向的结构长度的整数的可选指针
);

如果未发生错误, 则 accept 将返回一个类型为 SOCKET 的值,该值是新套接字的描述符。 此返回的值是实际连接的套接字的句柄。

否则,返回 INVALID_SOCKET 值,可以通过调用 WSAGetLastError 来检索特定的错误代码

addr返回的是一个客户端的地址, 如果对他不感兴趣, 可以设为NULL

举个例子

1
2
3
4
5
6
7
8
9
10
int len = sizeof(SOCKADDR);
SOCKADDR_IN addr;

m_socket = accept(m_socket, (SOCKADDR*)&addr, &len);
if (m_socket == -1)
{
perror("accept(): ");
std::cout << "Error Code : " << WSAGetLastError() << std::endl;
return 0;
}

客户端

connect()

1
2
3
4
5
int WSAAPI connect(
[in] SOCKET s, //标识未连接套接字的描述符
[in] const sockaddr *name, //指向应建立连接的 sockaddr 结构的指针
[in] int namelen //名称参数指向的 sockaddr 结构的长度(以字节为单位)
);

如果未发生错误, 连接 将返回零。 否则,它返回SOCKET_ERROR,可以通过调用 WSAGetLastError 来检索特定的错误代码

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port); // 16 位
inet_pton(AF_INET, PCSTR(ip.c_str()), &addr.sin_addr.S_un.S_addr);
//将点分十进制转化为二进制整数


if (connect(m_socket, (SOCKADDR*)&addr, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
perror("connect() :");
std::cout << "Error Code : " << WSAGetLastError() << std::endl;
return 0;
}

send 和 recv

用于发出和接收内容

1
2
3
4
5
6
7
8
9
10
char buffer[1024];
int sbytes = send(m_socket, buffer, len, 0);
if (sbytes == SOCKET_ERROR)
perror("send ()");
return sbytes;

int sbytes = recv(m_socket, buffer, maxlen, 0);
if (sbytes == SOCKET_ERROR)
perror("recv ()");
return sbytes;

UDP Socket编程

相比于TCP, UDP要简单的多

image-20221006152000858

主要学两个函数

先丢这里, 晚点看

函数: 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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等(谁发给你的)
addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
返回值说明:

读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。

注意:

由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
由于recvfrom函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct sockaddr_in类型进行强转。

函数: 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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入dest_addr结构体的长度。
返回值说明:

写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

注意:

由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
由于sendto函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct sockaddr_in类型进行强转