目录
一、socket 简介
二、socket 编程接口
1、socket()函数
domain参数
type参数
protocol参数
2、bind()函数
addr参数
addrlen参数
3、listen()函数
4、accept()函数
5、connect()函数
6、发送和接收函数
①recv()函数
②send()函数
7、htons()和 htonl()
三、IP 地址格式转换函数
inet_pton()函数
inet_ntop()函数
四、代码编写
验证
网络编程本就是一门非常难、非常深奥的技能,这里只是简单介绍和使用
套接字(socket)是 Linux 下的一种进程间通信机制(socket IPC),使用 socket IPC 可以使得在不同主机上的应用程序之间进行通信(网络通信),当然也可以是同一台主机上的不同应用程序。 socket IPC 通常使用(客户端<--->服务器)这种模式完成通信,多个客户端可以同时连接到服务器中,与服务器之间完成数据交互。
socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,它是一组接口。在设计模式中,
socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议隐藏在 socket 接口后面,对用户来说,一组简单的接口就是全部,让 socket 去组织数据,以符合指定的协议。所以,无需深入的去理解 tcp/udp 等各种复杂的 TCP/IP 协议, socket 已经为封装好了,只需要遵循 socket 的规定去编程,写出的程序自然遵循 tcp/udp 标准的。
当前网络中的主流程序设计都是使用 socket 进行编程的,因为它简单易用,它还是一个标准( BSD socket),能在不同平台很方便移植,比如你的一个应用程序是基于 socket 接口编写的,那么它可以移植到任何实现 BSD socket 标准的平台,如 Windows,它也实现了一套基于socket 的套接字接口
为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程
①、调用 socket()函数打开套接字;
②、调用 bind()函数将套接字与一个端口号以及 IP 地址进行绑定;
③、调用 listen()函数让服务器进程进入监听状态,监听客户端的连接请求;
④、调用 accept()函数处理到来的连接请求
int socket(int domain, int type, int protocol);
socket()函数类似于 open()函数,它用于创建一个网络通信端点(打开一个网络通信),如果成功则返回一个网络文件描述符,通常把这个文件描述符称为 socket 描述符(socket descriptor),这个 socket 描述符跟文件描述符一样,通过它来进行一些读写操作。
如果 socket()函数调用失败,则会返回-1,并且会设置 errno 变量以指示错误类型。
参数 domain 用于指定一个通信域; 这将选择将用于通信的协议族。可选的协议族部分如下
协议族名字 | 说明 | 帮助信息 |
AF_UNIX, AF_LOCAL | Local communication | unix(7) |
AF_INET | IPv4 Internet protocols | ip(7) |
AF_INET6 | IPv6 Internet protocols | ipv6(7) |
...... | ...... | ....... |
对于 TCP/IP 协议来说,通常选择 AF_INET 就可以了,如果你的 IP 协议的版本支持 IPv6,那么可以选择 AF_INET6。
参数 type 指定套接字的类型,当前支持的类型部分如下
type | 说明 |
SOCK_STREAM | 提供有序的、可靠的、双向的、基于连接的字节流,能保证数据 正确传送到对方,用于 TCP 协议;可以支持带外数据传输机制。 |
SOCK_DGRAM | 固定长度的、无连接的、不可靠的报文传递,用于 UDP 协议 |
SOCK_SEQPACKET | 固定长度的、有序的、可靠的、面向连接的报文传递 |
SOCK_RDM | 提供不保证排序的可靠数据报层。 |
...... | ........ |
参数 protocol 通常设置为 0,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 参数选择一个特定协议。在 AF_INET 通信域中,套接字类型为SOCK_STREAM 的默认协议是传输控制协议(Transmission Control Protocol, TCP 协议)。在 AF_INET 通信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP
bind()函数原型如下
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数用于将一个 IP 地址或端口号与一个套接字进行绑定(将套接字与地址进行关联)。一般来讲,会将一个服务器的套接字绑定到一个众所周知的地址---即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址(注意这里说的地址包括 IP 地址和端口号)。因为对于客户端来说,它与服务器进行通信,首先需要知道服务器的 IP 地址以及对应的端口号,所以通常服务器的 IP 地址以及端口号都是众所周知的
调用 bind()函数将参数 sockfd 指定的套接字与一个地址 addr 进行绑定,成功返回 0,失败情况下返回-1,并设置 errno 以提示错误原因
参数 addr 是一个指针,指向一个 struct sockaddr 类型变量
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
成员 sa_data 是一个 char 类型数组,一共 14 个字节,在这 14 个字节中就包括了 IP 地址、端口号等信息,这个结构对用户并不友好,把这些信息都封装在了 sa_data 数组中,这样使得用户是无法对sa_data 数组进行赋值
一般我们在使用的时候都会使用 struct sockaddr_in 结构体, sockaddr_in 和 sockaddr 是并列的结构(占用的空间是一样的),指向 sockaddr_in 的结构体的指针也可以指向 sockadd 的结构体,并代替它,而且sockaddr_in 结构对用户将更加友好,在使用的时候进行类型转换就可以了。该结构体内容如下
struct sockaddr_in {
sa_family_t sin_family; /* 协议族 */
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP 地址 */
unsigned char sin_zero[8];/*保留未用*/
};
参数 addrlen 指定了 addr 所指向的结构体对应的字节长度
listen()函数只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求, listen()函数在一般在 bind()函数之后调用,在 accept()函数之前调用
int listen(int sockfd, int backlog);
注意:无法在一个已经连接的套接字(即已经成功执行 connect()的套接字或由 accept()调用返回的套接字)上执行 listen()
参数 backlog 用来描述 sockfd 的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候,可能还存在其它的客户端请求建立连接,因为 TCP 连接是一个过程,由于同时尝试连接的用户过多,使得服务器进程无法快速地完成所有的连接请求,那怎么办呢?直接丢掉其他客户端的连接肯定不是一个很好的解决方法。
因此内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所
以必须有一个大小的上限,这个 backlog 参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理
服务器调用 listen()函数之后,就会进入到监听状态,等待客户端的连接请求,使用 accept()函数获取客户端的连接请求并建立连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()函数通常只用于服务器应用程序中,如果调用 accept()函数时,并没有客户端请求连接(等待连接队列中也没有等待连接的请求),此时 accept()会进入阻塞状态,直到有客户端连接请求到达为止。当有客户端连接请求到达时, accept()函数与远程客户端之间建立连接, accept()函数返回一个新的套接字。
这个套接字与 socket()函数返回的套接字并不同,以服务器为例,socket()函数返回的是服务器的套接字,而accept()函数返回的套接字连接到调用 connect()的客户端,服务器通过该套接字与客户端进行数据交互,譬如向客户端发送数据、或从客户端接收数据
参数 addr 是一个传出参数,参数 addr 用来返回已连接的客户端的 IP 地址与端口号等这些信息。参数addrlen 应设置为 addr 所指向的对象的字节长度,如果我们对客户端的 IP 地址与端口号这些信息不感兴趣,可以把 arrd 和 addrlen 均置为空指针 NULL。
如果 accept()函数执行出错,将会返回-1,并会设置 errno 以指示错误原因
connect()函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
该函数用于客户端应用程序中,客户端调用 connect()函数将套接字 sockfd 与远程服务器进行连接,参数 addr 指定了待连接的服务器的 IP 地址以及端口号等信息,参数 addrlen 指定了 addr 指向的 struct sockaddr对象的字节大小。
客户端通过 connect()函数请求与服务器建立连接,对于 TCP 连接来说,调用该函数将发生 TCP 连接的握手过程,并最终建立一个 TCP 连接,而对于 UDP 协议来说,调用这个函数只是在 sockfd 中记录服务器IP 地址与端口号,而不发送任何数据。
函数调用成功则返回 0,失败返回-1,并设置 errno 以指示错误原因
客户端与服务器建立好连接之后,我们就可以通过套接字描述符来收发数据了(对于客户端使用socket()返回的套接字描述符,而对于服务器来说,需要使用 accept()返回的套接字描述符),这与我们读写普通文件是差不多的操作,譬如可以调用 read()或 recv()函数读取网络数据,调用 write()或 send()函数发送数据。
原型如下
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
不论是客户端还是服务器都可以通过 revc()函数读取网络数据,它与 read()函数的功能是相似的。参数sockfd 指定套接字描述符,参数 buf 指向了一个数据接收缓冲区,参数 len 指定了读取数据的字节大小,参数 flags 可以指定一些标志用于控制如何接收数据。
标志 | 描述 |
MSG_CMSG_CLOEXEC | 为 UNIX 域套接字上接收的文件描述符设置执行时关闭标志 |
MSG_DONTWAIT | 启动非阻塞操作(相当于 O_NONBLOCK) |
MSG_ERRQUEUE | 接收错误信息作为辅助数据 |
MSG_OOB | 如果协议支持,获取带外数据 |
MSG_PEEK | 返回数据包内容而不真正取走数据包 |
MSG_TRUNC | 即使数据包被截断,也返回数据包的长度 |
MSG_WAITALL | 等待知道所有的数据可用(仅 SOCK_STREAM) |
一般将 flags 参数设置为 0
如果发送者已经调用 shutdown 来结束传输,或者网络协议支持按默认的顺序关闭并且发送端已经关闭,那么当所有的数据接收完毕后, recv 会返回 0。recv 在调用成功情况下返回实际读取到的字节数。
原型如下
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
send 可以通过参数 flags 指定一些标志,来改变处理传输数据的方式
标志 | 描述 |
MSG_CONFIRM | 提供链路层反馈以保持地址映射有效 |
MSG_DONTROUTE | 勿将数据包路由出本地网络 |
MSG_DONTWAIT | 允许非阻塞操作(等价于使用 O_NONBLOCK) |
MSG_EOR | 如果协议支持,标志记录结束 |
MSG_MORE | 延迟发送数据包允许写更多数据 |
MSG_NOSIGNAL | 在写无连接的套接字时不产生 SIGPIPE 信号 |
MSG_OOB | 如果协议支持,发送带外数据 |
send()成功返回,也并不表示连接的另一端的进程就一定接收了数据,我们所能保证的只是当 send成功返回时,数据已经被无错误的发送到网络驱动程序上
这是两个宏
htonl()
是一个函数,用于将一个32位的IP地址从主机字节序转换为网络字节序,其声明在头文件 netinet/in.h
中。
INADDR_ANY
是一个常量,它表示将 socket
绑定到所有可用的网络接口,即服务器将监听所有可用的IP地址。使用 INADDR_ANY
可以使服务器监听所有的IP地址,而不用在代码中为每个IP地址都创建一个套接字。一般搭配为htonl(INADDR_ANY) 。返回值:htonl()返回一个网络字节顺序的值。
htons
是一个函数,用于将一个 16 位的整数由主机字节序(即本地计算机的字节序)转换为网络字节序(即大端字节序)。由于不同的计算机具有不同的字节序,因此必须使用 htons
将其转换为网络字节序,以确保客户端与服务器之间的通信使用的是相同的字节序
192.168.1.110、 192.168.1.50点分十进制的 IP 地址,这其实是一种字符串的形式,但是计算机所需要理解的是二进制形式的 IP 地址,所以我们就需要在点分十进制字符串和二进制地址之间进行转换
原型如下
int inet_pton(int af, const char *src, void *dst);
将点分十进制表示的字符串形式转换成二进制 Ipv4 或 Ipv6 地址
将字符串 src 转换为二进制地址,参数 af 必须是 AF_INET 或 AF_INET6, AF_INET 表示待转换的 Ipv4地址, AF_INET6 表示待转换的是 Ipv6 地址;并将转换后得到的地址存放在参数 dst 所指向的对象中,如果参数 af 被指定为 AF_INET,则参数 dst 所指对象应该是一个 struct in_addr 结构体的对象;如果参数 af 被指定为 AF_INET6,则参数 dst 所指对象应该是一个 struct in6_addr 结构体的对象
客户端需要将字符串形式的IP地址转换为网络字节序的二进制IP地址,以便能够通过socket_fd套接字与服务器进行通信。因此,客户端需要使用inet_pton()函数将字符串形式的IP地址转换为二进制IP地址。
inet_pton()转换成功返回 1(已成功转换)。如果 src 不包含表示指定地址族中有效网络地址的字符串,则返回 0。如果 af 不包含有效的地址族,则返回-1 并将 errno 设置为 EAFNOSUPPORT
inet_ntop()函数执行与 inet_pton()相反的操作,函数原型如下
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数 af 与 inet_pton()函数的 af 参数意义相同。参数 src 应指向一个 struct in_addr 结构体对象或 struct in6_addr 结构体对象,依据参数 af 而定。函数inet_ntop()会将参数 src 指向的二进制 IP 地址转换为点分十进制形式的字符串,并将字符串存放在参数 dts所指的缓冲区中,参数 size 指定了该缓冲区的大小。
inet_ntop()
函数是将 IP 地址从二进制格式转换成文本格式的函数,它通常在服务端中使用,用于将客户端的 IP 地址转换成可读的形式,以便在服务端记录日志或者其他操作中使用。
inet_ntop()在成功时会返回 dst 指针。如果 size 的值太小了,那么将会返回 NULL 并将 errno 设置为ENOSPC
服务器
socket创建一个套接字,使用IPv4协议,流式套接字,即基于TCP协议的连接导向的、可靠的、面向字节流的套接字。bind将套接字绑定到指定的地址和端口号上,listen将套接字设置为监听状态,等待客户端连接,accept阻塞等待客户端连接,并返回一个新的套接字描述符用于与客户端通信,使用接受函数recv()
从已连接套接字读取数据,使用close()
关闭套接字。
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 6666 // 端口号不能发生冲突,不常用的端口号通常大于 5000
#define BUFSIZE 512
int main()
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
char ip_str[20] = {0};
int socket_fd, connect_fd;
int addr_len = sizeof(client_addr);
char recvbuf[BUFSIZE];
int ret;
/* 打开套接字,得到套接字描述符
* AF_INET : IPv4协议
* SOCK_STREAM : 用于tcp协议的套接字类型
* 0 :默认协议
*/
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > socket_fd)
{
perror("socket error");
exit(EXIT_FAILURE);
}
/* 将套接字与指定端口号进行绑定 */
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
if ((ret = bind(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))) < 0)
{
perror("bind error");
close(socket_fd);
exit(EXIT_FAILURE);
}
/* 使服务器进入监听状态 */
if ((ret = listen(socket_fd, 50)) < 0)
{
perror("listen error");
close(socket_fd);
exit(EXIT_FAILURE);
}
/* 阻塞等待客户端连接 */
connect_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &addr_len);
if (connect_fd < 0)
{
perror("accept error");
close(socket_fd);
exit(EXIT_FAILURE);
}
printf("有客户端接入...\n");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
printf("客户端主机的 IP 地址: %s\n", ip_str);
printf("客户端进程的端口号: %d\n", client_addr.sin_port);
/* 接收客户端发送过来的数据 */
for (;;)
{
// 接收缓冲区清零
memset(recvbuf, 0x0, sizeof(recvbuf));
// 读数据
if ((ret = recv(connect_fd, recvbuf, sizeof(recvbuf), 0)) <= 0)
{
perror("recv error");
close(connect_fd);
break;
}
// 将读取到的数据以字符串形式打印出来
printf("from client string: %s\n", recvbuf);
// 如果读取到"exit"则关闭套接字退出程序
if (strncmp("exit", recvbuf, 4) == 0)
{
printf("server exit...\n");
close(connect_fd);
break;
}
}
/* 关闭套接字 */
close(socket_fd);
exit(EXIT_SUCCESS);
}
客户端
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 6666 // 服务器的端口号
#define SERVER_IP "192.168.1.221" // 服务器的 IP 地址
#define BUFSIZE 512
int main()
{
struct sockaddr_in server_addr = {0};
char buf[BUFSIZE];
int socket_fd, ret;
/* 打开套接字,得到套接字描述符 */
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0)
{
perror("socket error");
exit(EXIT_FAILURE);
}
/* 调用 connect 连接远端服务器 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); // 端口号
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr); // IP 地址
if ((ret = connect(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))) < 0)
{
perror("connect error");
close(socket_fd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");
/* 向服务器发送数据 */
for (;;)
{
// 清理缓冲区
memset(buf, 0, sizeof(buf));
// 接收用户输入的字符串数据
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
// 将用户输入的数据发送给服务器
if ((ret = send(socket_fd, buf, sizeof(buf), 0)) < 0)
{
perror("send error");
break;
}
// 输入了"exit",退出循环
if (strncmp(buf, "exit", 4) == 0)
break;
}
close(socket_fd);
exit(EXIT_SUCCESS);
}
客户端代码和服务器的代码差不多, 需要注意要和端口的一样
在开发板上面运行服务器端,在ubantu运行客户端给服务器端发信息