相关结构体与函数
sockaddr、sockaddr_in结构体
sockaddr和sockaddr_in详解
struct sockaddr
共16字节,协议族(family)占2字节,IP地址和端口号在sa_data字符数组中
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON(sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
struct sockaddr_in
更细致地划分了协议族、端口号和IP地址,其中IP地址定义了新的结构体struct in_addr
,该结构体中宏定义了uint32_t
类型的变量,sin_zero字符数组存在的意义是为了使struct sockaddr_in
和struct sockaddr
大小相等,便于进行强制类型转换(与bind()
等函数的参数有关)
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON(sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof(struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof(in_port_t) -
sizeof(struct in_addr)];
};
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
总结
- 二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr
- sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址
- sockaddr_in 是internet环境下套接字的地址形式
- 在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化
- 一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数
socket()函数
socket函数用于创建一个新的socket,也就是向系统申清一个socket资源
socket函数用户客户端和服务端
int socket(int domain, int type, int protocol);
domain:协议域,又称协议族(family)。常用的协议族有AF INET、AF INET6、AF LOCAL(或称AF UNIX,Unix域Socket)、AF ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址
type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用
protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
💡 第一个参数只能填AF INET,第二个参数只能填SOCK STREAM,第三个参数只填0
除非系统资料耗尽,socket函数一般不会返回失败
返回值:成功则返回一个socket,失败返回-1,错误原因存于errno中
💡 “资源耗尽”即Linux对打开文件数的限制,见2022-06-10笔记
// 第1步:创建服务端的socket
int listenfd;
for (int i = 0; i < 2000; ++i)
{
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
return -1;
}
cout << "sock id:" << listenfd << endl;
}
sock id:1012
sock id:1013
sock id:1014
sock id:1015
sock id:1016
sock id:1017
sock id:1018
sock id:1019
sock id:1020
sock id:1021
sock id:1022
sock id:1023
socket: Too many open files
inet_addr() 和inet_ntoa()函数
使用socket进行通信的时候,我们需要指定三个元素:通信域(地址族)、IP地址、端口号,这三个元素由SOCKADDR_IN结构体定义
为了简化编程一般将IP地址设置为INADDR_ANY,如果需要使用特定的IP地址则需要使用inet_addr()
** 和**inet_ntoa()
函数
inet_addr()
和inet_ntoa()
完成字符串和in_addr结构体的互换
inet_addr()
函数参数cp代表点分十进制的IP地址,如1.2.3.4,返回值为in_addr_t
类型
/* Convert Internet host address from numbers-and-dots notation in CP
into binary data in network byte order. */
extern in_addr_t inet_addr (const char *__cp) __THROW;
inet_ntoa()
函数输入为in_addr结构体而输出为字符串
/* Convert Internet number in IN to ASCII representation. The return value
is a pointer to an internal array containing the string. */
extern char *inet_ntoa (struct in_addr __in) __THROW;
hostent结构体
hostent实例详解
/* Description of data base entry for a single host. */
struct hostent
{
char *h_name; /* Official name of host. */
char **h_aliases; /* Alias list. */
int h_addrtype; /* Host address type. */
int h_length; /* Length of address. */
char **h_addr_list; /* List of addresses from name server. */
#ifdef __USE_MISC
# define h_addr h_addr_list[0] /* Address, for backward compatibility.*/
#endif
};
struct hostent
{
char *h_name; //正式主机名
char **h_aliases; //主机别名
int h_addrtype; //主机IP地址类型:IPV4-AF_INET
int h_length; //主机IP地址字节长度,对于IPv4是四字节,即32位
char **h_addr_list; //主机的IP地址列表
};
#define h_addr h_addr_list[0] //保存的是IP地址
- h_name:官方域名(Official domain name)。官方域名代表某一主页,但实际上一些著名公司的域名并未用官方域名注册
- h_aliases:别名,可以通过多个域名访问同一主机。同一 IP 地址可以绑定多个域名,因此除了当前域名还可以指定其他域名
- h_addrtype:gethostbyname() 不仅支持 IPv4,还支持 IPv6,可以通过此成员获取IP地址的地址族(地址类型)信息,IPv4 对应 AF_INET,IPv6 对应 AF_INET6
- h_length:保存IP地址长度。IPv4 的长度为 4 个字节,IPv6 的长度为 16 个字节
- h_addr_list:这是最重要的成员。通过该成员以整数形式保存域名对应的 IP 地址。对于用户较多的服务器,可能会分配多个 IP 地址给同一域名,
利用多个服务器进行均衡负载
在实际的应用中,一台服务器往往有好几个IP地址,而域名只有一个
,这样设计的好处是,可以使系统分布设计,提升服务器的稳定性和抗灾难能力
一般对服务器的访问,则是先经过DNS(Domain Name System)
服务器,DNS通过均衡设计,返回合适的IP与客户端进行交互,避免客户端只连接一个IP,导致网络拥堵
gethostbyname()函数
gethostbyname()函数:通过域名获取IP地址
gethostbyname()函数详解
客户端中直接使用IP地址会有很大的弊端,一旦IP地址变化(IP地址会经常变动),客户端软件就会出现错误
而使用域名会方便很多,注册后的域名只要每年续费就永远属于自己的,更换IP地址时修改域名解析即可,不会影响软件的正常使用
域名仅仅是 IP 地址的一个助记符,目的是方便记忆,通过域名并不能找到目标计算机,通信之前必须要将域名转换成 IP 地址
gethostbyname() 函数可以完成这种转换
/* Return entry from host data base for host with NAME.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern struct hostent *gethostbyname (const char *__name);
bind()函数
服务端用于将把用于通信的地址和端口绑定到socket上
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:需要绑定的socket
addr:存放了服务端用于通信的地址和端口
addrlen:表示addr结构体的大小
返回值:成功则返回0,失败返回-1,错误原因存于errno中
如果绑定的地址错误,或端口已被占用,bind函数一定会报错,否则一般不会返回错误
💡 注意第二个参数为sockaddr
结构体指针
listen()函数
listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程
在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接
/* Prepare to accept connections on socket FD.
N connection requests will be queued before further requests are refused.
Returns 0 on success, -1 for errors. */
extern int listen (int __fd, int __n) __THROW;
_fd:服务端的socket,标识绑定的,未连接的套接字的描述符
-n:挂起的连接队列的最大长度
- 比如有100个用户链接请求,但是系统一次只能处理20个,那么剩下的80个不能不理人家,所以系统就创建个队列记录这些暂时不能处理,一会儿处理的连接请求,依先后顺序处理,那这个队列到底多大?就是这个参数设置,比如2,那么就允许两个新链接排队。这个不能无限大,那内存就不够了
- 可以手动设置这个参数,但是别太大
- 我们一般填写这个参数为
SOMAXCONN
,让系统自动选择最合适的个数
connect()函数
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
第一个参数:int sockdf:
socket文件描述符
第二个参数: const struct sockaddr *addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
第三个参数:socklen_t addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功: 0
失败:-1,设置errno
当客户端调用 connect()函数之后,发生一下情况之一才会返回(完成函数调用)
- 服务器端接收连接请求
- 发生断网的异常情况而终端连接请求
需要注意的是,所谓的“接收连接”并不意味着服务器调用 accept()函数,其实是服务器端把连接请求信息记录到等待队列,因此 connect()函数返回后并不进行数据交换,而是要等服务器端 accept 之后才能进行数据交换(read、write)
客户端端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址
accept()函数
socket的accept函数解析
socket中accept()函数的理解
/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int accept (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __addr_len);
send()函数
send函数用于把数据通过socket发送给对端
不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd为已建立好连接的socket
- buf为需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么
- len需要发送的数据的长度,为buf中有效数据的长度
- flags填0,其他数值意义不大
- 函数返回已发送的字符数,出错时返回-1,错误信息errno被标记
- 注意,就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错
- 如果send函数返回的错误(<=0),表示通信链路已不可用
recv()函数
recv函数用于接收对端通过socket发送过来的数据
不论是客户端还是服务端,应用程序都用recv函数接收来自TCP连接的另一端发送过来数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数信息同send()函数
函数返回已接收的字符数,出错时返回-1,失败时不会设置errno的值
- 如果socket的对端没有发送数据,recv函数就会等待
- 如果对端发送了数据,函数返回接收到的字符数
- 出错时返回-1,如果socket被对端关闭,返回值为0
- 如果recv函数返回的错误(<=0),表示通信通道已不可用
💡 数据收发时的数据量会受到发送缓冲区和接收缓冲区大小的限制
数据收发时要注意字节序的问题(不同主机字节序的问题)