LINUX应用编程 第二十九章 socket 编程基础 学习笔记

socket 编程基础
Linux 系统是依靠互联网平台迅速发展起来的,所以它具有强大的网络功能支持,也是 Linux 系统的一大特点。互联网对人类社会产生了巨大影响,它几乎改变了人们生活的方方面面,可见互联网对人类社会的重要性!
本章我们便来学习 Linux 下的网络编程,我们一般称为 socket 编程,在上一章中给大家介绍过,socket是内核向应用层提供的一套网络编程接口,用户基于 socket 接口可开发自己的网络相关应用程序。
本章作为网络编程基础内容,其中并不会深入、详细地介绍 socket 编程,旨在以引导大家入门为主;其一在于网络编程本就是一门非常难、非常深奥的技能,市面上有很多关于 Linux/UNIX 网络编程类书籍,这些书籍专门介绍了网络编程相关知识内容,而且书本非常厚,可将其内容之多、难点之多;其二在于笔者对网络编程了解知之甚少,掌握的知识、技能太少,无法向大家传授更加深入的知识、内容,如果大家以后有机会从事网络编程开发相关工作,可以购买此类书籍深入学习、研究。



本章将会讨论如下主题内容。
 socket 简介
 socket 编程 API 介绍



29.1 socket 简介
套接字(socket)是 Linux 下的一种进程间通信机制(socket IPC),在前面的内容中已经给大家提到过,
使用 socket IPC 可以使得在不同主机上的应用程序之间进行通信(网络通信),当然也可以是同一台主机上的不同应用程序。socket IPC 通常使用客户端<--->服务器这种模式完成通信,多个客户端可以同时连接到服务器中,与服务器之间完成数据交互。
内核向应用层提供了 socket 接口,对于应用程序开发人员来说,我们只需要调用 socket 接口开发自己的应用程序即可!socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,它是一组接口。在设计模式中,
socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议隐藏在 socket 接口后面,对用户来说,一组简单的接口就是全部,让 socket 去组织数据,以符合指定的协议。所以,我们无需深入的去理解 tcp/udp 等各种复杂的 TCP/IP 协议,socket 已经为我们封装好了,我们只需要遵循 socket 的规定去编程,写出的程序自然遵循 tcp/udp 标准的。
当前网络中的主流程序设计都是使用 socket 进行编程的,因为它简单易用,它还是一个标准(BSDsocket),能在不同平台很方便移植,比如你的一个应用程序是基于 socket 接口编写的,那么它可以移植到任何实现 BSD socket 标准的平台,譬如 LwIP,它兼容 BSD Socket;又譬如 Windows,它也实现了一套基于socket 的套接字接口,更甚至在国产操作系统中,如 RT-Thread,它也实现了 BSD socket 标准的 socket 接口。



29.2 socket 编程 编程 接口介绍
本小节我们向大家介绍,socket 编程中使用到的一些接口函数。使用 socket 接口需要在我们的应用程序代码中包含两个头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
socket() 函数
socket()函数原型如下所示:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函数类似于 open()函数,它用于创建一个网络通信端点(打开一个网络通信),如果成功则返回一个网络文件描述符,通常把这个文件描述符称为 socket 描述符(socket descriptor),这个 socket 描述符跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。


该函数包括 3 个参数,如下所示:
domain
参数 domain 用于指定一个通信域;这将选择将用于通信的协议族。可选的协议族如下表所示:
协议族名字 说明 帮助信息
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25/ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK AppleTalk ddp(7)
AF_PACKET Low level packet interface packet(7)
AF_ALG Interface to kernel crypto API
表 29.2.1 协议族描述
对于 TCP/IP 协议来说,通常选择 AF_INET 就可以了,当然如果你的 IP 协议的版本支持 IPv6,那么可以选择 AF_INET6。


type
参数 type 指定套接字的类型,当前支持的类型有:
type 说明
SOCK_STREAM 提供有序的、可靠的、双向的、基于连接的字节流,能保证数据正确传送到对方,用于 TCP 协议;可以支持带外数据传输机制。
SOCK_DGRAM 固定长度的、无连接的、不可靠的报文传递,用于 UDP 协议
SOCK_SEQPACKET 固定长度的、有序的、可靠的、面向连接的报文传递
SOCK_RAW 表示原始套接字,它允许应用程序访问网络层的原始数据包,这
个套接字用得比较少,暂时不用理会它。
SOCK_RDM 提供不保证排序的可靠数据报层。
SOCK_PACKET 已过时,不应在应用程序中使用
表 29.2.2 套接字类型描述


protocol
参数 protocol 通常设置为 0,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 参数选择一个特定协议。在 AF_INET 通信域中,套接字类型为SOCK_STREAM 的默认协议是传输控制协议(Transmission Control Protocol,TCP 协议)。在 AF_INET 通信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP。
调用 socket()与调用 open()函数很类似,调用成功情况下,均会返回用于文件 I/O 的文件描述符,只不过对于 socket()来说,其返回的文件描述符一般称为 socket 描述符。当不再需要该文件描述符时,可调用close()函数来关闭套接字,释放相应的资源。


如果 socket()函数调用失败,则会返回-1,并且会设置 errno 变量以指示错误类型。


使用示例

int socket_fd = socket(AF_INET, SOCK_STREAM, 0);//打开套接字
if (0 > socket_fd) {
perror("socket error");
exit(-1);
}
......
......
close(socket_fd); //关闭套接字

 



bind() 函数
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 类型变量,如下所示:
示例代码 29.2.1 struct sockaddr 结构体

struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}

 


第二个成员 sa_data 是一个 char 类型数组,一共 14 个字节,在这 14 个字节中就包括了 IP 地址、端口号等信息,这个结构对用户并不友好,它把这些信息都封装在了 sa_data 数组中,这样使得用户是无法对sa_data 数组进行赋值。事实上,这是一个通用的 socket 地址结构体。
一般我们在使用的时候都会使用 struct sockaddr_in 结构体,sockaddr_in 和 sockaddr 是并列的结构(占用的空间是一样的),指向 sockaddr_in 的结构体的指针也可以指向 sockadd 的结构体,并代替它,而且sockaddr_in 结构对用户将更加友好,在使用的时候进行类型转换就可以了。该结构体内容如下所示:


示例代码 29.2.2 struct 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];
};

 


这个结构体的第一个字段是与 sockaddr 结构体是一致的,而剩下的字段就是 sa_data 数组连续的 14 字节信息里面的内容,只不过从新定义了成员变量而已,sin_port 字段是我们需要填写的端口号信息,sin_addr字段是我们需要填写的 IP 地址信息,剩下 sin_zero 区域的 8 字节保留未用。
最后一个参数 addrlen 指定了 addr 所指向的结构体对应的字节长度。


使用示例
struct sockaddr_in socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr)); //清零
//填充变量
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);

//将地址与套接字进行关联、绑定
bind(socket_fd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
注意,代码中的 htons 和 htonl 并不是函数,只是一个宏定义,主要的作用在于为了避免大小端的问题,
需要这些宏需要在我们的应用程序代码中包含头文件<netinet/in.h>。
Tips:bind()函数并不是总是需要调用的,只有用户进程想与一个具体的 IP 地址或端口号相关联的时候才需要调用这个函数。如果用户进程没有这个必要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择,通常在客户端应用程序中会这样做。


listen() 函数
listen()函数只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,listen()函数在一般在 bind()函数之后调用,在 accept()函数之前调用,它的函数原型是:
int listen(int sockfd, int backlog);
无法在一个已经连接的套接字(即已经成功执行 connect()的套接字或由 accept()调用返回的套接字)上执行 listen()。
参数 backlog 用来描述 sockfd 的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候,可能还存在其它的客户端请求建立连接,因为 TCP 连接是一个过程,由于同时尝试连接的用户过多,使得服务器进程无法快速地完成所有的连接请求,那怎么办呢?直接丢掉其他客户端的连接肯定不是一个很好的解决方法。因此内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限,这个 backlog 参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。


accept() 函数
服务器调用 listen()函数之后,就会进入到监听状态,等待客户端的连接请求,使用 accept()函数获取客户端的连接请求并建立连接。函数原型如下所示:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程:
①、调用 socket()函数打开套接字;
②、调用 bind()函数将套接字与一个端口号以及 IP 地址进行绑定;
③、调用 listen()函数让服务器进程进入监听状态,监听客户端的连接请求;
④、调用 accept()函数处理到来的连接请求。


accept()函数通常只用于服务器应用程序中,如果调用 accept()函数时,并没有客户端请求连接(等待连接队列中也没有等待连接的请求),此时 accept()会进入阻塞状态,直到有客户端连接请求到达为止。当有客户端连接请求到达时,accept()函数与远程客户端之间建立连接,accept()函数返回一个新的套接字。这个套接字与 socket()函数返回的套接字并不同,socket()函数返回的是服务器的套接字(以服务器为例),而accept()函数返回的套接字连接到调用 connect()的客户端,服务器通过该套接字与客户端进行数据交互,譬如向客户端发送数据、或从客户端接收数据。


所以,理解 accept()函数的关键点在于它会创建一个新的套接字,其实这个新的套接字就是与执行connect()(客户端调用 connect()向服务器发起连接请求)的客户端之间建立了连接,这个套接字代表了服务器与客户端的一个连接。如果 accept()函数执行出错,将会返回-1,并会设置 errno 以指示错误原因。
参数 addr 是一个传出参数,参数 addr 用来返回已连接的客户端的 IP 地址与端口号等这些信息。参数addrlen 应设置为 addr 所指向的对象的字节长度,如果我们对客户端的 IP 地址与端口号这些信息不感兴趣,
可以把 arrd 和 addrlen 均置为空指针 NULL。

connect() 函数
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()函数发送数据。


read() 函数
read()函数大家都很熟悉了,通过 read()函数从一个文件描述符中读取指定字节大小的数据并放入到指定的缓冲区中,read()调用成功将返回读取到的字节数,此返回值受文件剩余字节数限制,当返回值小于指定的字节数时并不意味着错误;这可能是因为当前可读取的字节数小于指定的字节数(比如已经接近文件结尾,或者正在从管道或者终端读取数据,或者 read()函数被信号中断等),出错返回-1 并设置 errno,如果在调 read 之前已到达文件末尾,则这次 read 返回 0。


套接字描述符也是文件描述符,所以使用 read()函数读取网络数据时,read()函数的参数 fd 就是对应的套接字描述符。


recv() 函数
recv()函数原型如下所示:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
不论是客户端还是服务器都可以通过 revc()函数读取网络数据,它与 read()函数的功能是相似的。参数sockfd 指定套接字描述符,参数 buf 指向了一个数据接收缓冲区,参数 len 指定了读取数据的字节大小,参数 flags 可以指定一些标志用于控制如何接收数据。
函数 recv()与 read()很相似,但是 recv()可以通过指定 flags 标志来控制如何接收数据,这些标志如下所示:
标志 描述
MSG_CMSG_CLOEXEC 为 UNIX 域套接字上接收的文件描述符设置执行时关闭标志MSG_DONTWAIT 启动非阻塞操作(相当于 O_NONBLOCK)
MSG_ERRQUEUE 接收错误信息作为辅助数据
MSG_OOB 如果协议支持,获取带外数据
MSG_PEEK 返回数据包内容而不真正取走数据包
MSG_TRUNC 即使数据包被截断,也返回数据包的长度
MSG_WAITALL 等待知道所有的数据可用(仅 SOCK_STREAM)
表 29.2.3 recv 函数标志描述

通常一般我们将 flags 参数设置为 0,当然,你可以根据自己的需求设置该参数。
当指定 MSG_PEEK 标志时,可以查看下一个要读取的数据但不真正取走它,当再次调用 read 或 recv函数时,会返回刚才查看的数据。
对于 SOCK_STREAM 类型套接字,接收的数据可以比指定的字节大小少。MSG_WAITALL 标志会阻止这种行为,知道所请求的数据全部返回,recv 函数才会返回。对于 SOCK_DGRAM 和 SOCK_SEQPACKET
套接字,MSG_WAITALL 标志并不会改变什么行为,因为这些基于报文的套接字类型一次读取就返回整个报文。


如果发送者已经调用 shutdown 来结束传输,或者网络协议支持按默认的顺序关闭并且发送端已经关闭,
那么当所有的数据接收完毕后,recv 会返回 0。


recv 在调用成功情况下返回实际读取到的字节数。
write() 函数
通过 write()函数可以向套接字描述符中写入数据,函数调用成功返回写入的字节数,失败返回-1,并设置 errno 变量。
send() 函数
函数原型如下所示:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
send 和 write 很相似,但是 send 可以通过参数 flags 指定一些标志,来改变处理传输数据的方式。这些标志如下所示:
标志 描述
MSG_CONFIRM 提供链路层反馈以保持地址映射有效
MSG_DONTROUTE 勿将数据包路由出本地网络
MSG_DONTWAIT 允许非阻塞操作(等价于使用 O_NONBLOCK)
MSG_EOR 如果协议支持,标志记录结束
MSG_MORE 延迟发送数据包允许写更多数据
MSG_NOSIGNAL 在写无连接的套接字时不产生 SIGPIPE 信号
MSG_OOB 如果协议支持,发送带外数据
表 29.2.4 send 函数标志描述
即使 send()成功返回,也并不表示连接的另一端的进程就一定接收了数据,我们所能保证的只是当 send
成功返回时,数据已经被无错误的发送到网络驱动程序上。


close() 关闭套接字
当不再需要套接字描述符时,可调用 close()函数来关闭套接字,释放相应的资源。


29.3 IP 地址格式转换函数
对于人来说,我们更容易阅读的是点分十进制的 IP 地址,譬如 192.168.1.110、192.168.1.50,这其实是一种字符串的形式,但是计算机所需要理解的是二进制形式的 IP 地址,所以我们就需要在点分十进制字符串和二进制地址之间进行转换。
点分十进制字符串和二进制地址之间的转换函数主要有:inet_aton、inet_addr、inet_ntoa、inet_ntop、inet_pton 这五个,在我们的应用程序中使用它们需要包含头文件<sys/socket.h>、<arpa/inet.h>以及<netinet/in.h>。

inet_aton 、inet_addr 、inet_ntoa 函数
这些函数可将一个 IP 地址在点分十进制表示形式和二进制表示形式之间进行转换,这些函数已经废弃了,基本不用这些函数了,但是在一些旧的代码中可能还会看到这些函数。完成此类转换工作我们应该使用下面介绍的这些函数。


inet_ntop 、inet_pton 函数
inet_ntop()、inet_pton()与 inet_ntoa()、inet_aton()类似,但它们还支持 IPv6 地址。它们将二进制 Ipv4 或Ipv6 地址转换成以点分十进制表示的字符串形式,或将点分十进制表示的字符串形式转换成二进制 Ipv4 或Ipv6 地址。使用这两个函数只需包含<arpa/inet.h>头文件即可!


inet_pton() 函数
inet_pton()函数原型如下所示:
int inet_pton(int af, const char *src, void *dst);
inet_pton()函数将点分十进制表示的字符串形式转换成二进制 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 结构体的对象。
inet_pton()转换成功返回 1(已成功转换)。如果 src 不包含表示指定地址族中有效网络地址的字符串,
则返回 0。如果 af 不包含有效的地址族,则返回-1 并将 errno 设置为 EAFNOSUPPORT。


使用示例
示例代码 29.3.1 inet_pton()函数使用示例

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#define IPV4_ADDR "192.168.1.222"
int main(void)
{
struct in_addr addr;
inet_pton(AF_INET, IPV4_ADDR, &addr);
printf("ip addr: 0x%x\n", addr.s_addr);
exit(0);
}

 


测试结果:

图 29.3-1 测试结果


inet_ntop() 函数
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()在成功时会返回 dst 指针。如果 size 的值太小了,那么将会返回 NULL 并将 errno 设置为ENOSPC。
使用示例
示例代码 29.3.2 inet_ntop()函数使用示例

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main(void)
{
struct in_addr addr;
char buf[20] = {0};
addr.s_addr = 0xde01a8c0;
inet_ntop(AF_INET, &addr, buf, sizeof(buf));
printf("ip addr: %s\n", buf);
exit(0);
}

 


测试结果:
图 29.3-2 测试结果



29.4 socket 编程实战
经过上面的介绍,本小节我们将进行编程实战,实现一个简单地服务器和一个简单地客户端应用程序。
编写服务器程序
编写服务器应用程序的流程如下:
①、调用 socket()函数打开套接字,得到套接字描述符;
②、调用 bind()函数将套接字与 IP 地址、端口号进行绑定;
③、调用 listen()函数让服务器进程进入监听状态;
④、调用 accept()函数获取客户端的连接请求并建立连接;
⑤、调用 read/recv、write/send 与客户端进行通信;
⑥、调用 close()关闭套接字。
下面,我们就根据上面列举的步骤来编写一个简答地服务器应用程序,代码如下所示:
本 例 程 源 码 对 应 的 路 径 为 : 开 发 板 网 盘 -> 例 程 源 码 ->Linux C 应 用 编 程 例 程 源 码->30_socket->socket_server.c。
示例代码 29.4.1 简单地服务器应用程序示例代码

/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
文件名 : socket_server.c
作者 : 邓涛
版本 : V1.0
描述 : 一个简单地 TCP 服务器应用程序示例代码
其他 : 无
论坛 : www.openedv.com
日志 : 初版 V1.0 2021/7/20 邓涛创建
***************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //端口号不能发生冲突,不常用的端口号通常大于 5000
int main(void)
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
char ip_str[20] = {0};

int sockfd, connfd;
int addrlen = sizeof(client_addr);
char recvbuf[512];
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
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);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 使服务器进入监听状态 */
ret = listen(sockfd, 50);
if (0 > ret) {
perror("listen error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 阻塞等待客户端连接 */
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (0 > connfd) {
perror("accept error");
close(sockfd);
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));
// 读数据
ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
if(0 >= ret) {
perror("recv error");
close(connfd);
break;
}
// 将读取到的数据以字符串形式打印出来
printf("from client: %s\n", recvbuf);
// 如果读取到"exit"则关闭套接字退出程序
if (0 == strncmp("exit", recvbuf, 4)) {
printf("server exit...\n");
close(connfd);
break;
}
}
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}

 


以上我们实现了一个非常简单地服务器应用程序,根据上面列举的步骤完成了这个示例代码,最终的功能是,当客户端连接到服务器之后,客户端会向服务器(也就是本程序)发送数据,在我们服务器应用程序中会读取客户端发送的数据并将其打印出来,就是这么简单的一个功能。
SERVER_PORT 宏指定了本服务器绑定的端口号,这里我们将端口号设置为 8888,端口不能与其它服务器的端口号发生冲突,不常用的端口号通常大于 5000。
代码就不再解释了,都非常简单!


编写客户端程序
接下来我们再编写一个简单地客户端应用程序,客户端的功能是连接上小节所实现的服务器,连接成功之后向服务器发送数据,发送的数据由用户输入。示例代码如下所示:
本 例 程 源 码 对 应 的 路 径 为 : 开 发 板 网 盘 -> 例 程 源 码 ->Linux C 应 用 编 程 例 程 源 码->30_socket->socket_client.c。

示例代码 29.4.2 简单地客户端应用程序示例代码

/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
文件名 : socket_client.c
作者 : 邓涛
版本 : V1.0
描述 : 一个简单地 TCP 客户端应用程序示例代码
其他 : 无
论坛 : www.openedv.com
日志 : 初版 V1.0 2021/7/20 邓涛创建
***************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //服务器的端口号
#define SERVER_IP "192.168.6.253" //服务器的 IP 地址
int main(void)
{
struct sockaddr_in server_addr = {0};
char buf[512];
int sockfd;
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
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 地址
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("connect error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");
/* 向服务器发送数据 */
for ( ; ; ) {
// 清理缓冲区
memset(buf, 0x0, sizeof(buf));
// 接收用户输入的字符串数据
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
// 将用户输入的数据发送给服务器
ret = send(sockfd, buf, strlen(buf), 0);
if(0 > ret){
perror("send error");
break;
}
//输入了"exit",退出循环
if(0 == strncmp(buf, "exit", 4))
break;
}
close(sockfd);
exit(EXIT_SUCCESS);
}

 


代码不再说明!需要注意的是 SERVER_IP 和 SERVER_PORT 指的是服务器的 IP 地址和端口号,服务器的 IP 地址根据实际情况进行设置,服务器应用程序示例代码 29.4.1 中我们绑定的端口号为 8888,所以在客户端应用程序中我们也需要指定 SERVER_PORT 为 8888。


编译测试
这里笔者将服务器程序运行在开发板上,而将客户端应用程序运行在 Ubuntu 系统,当然你也可以将客户端和服务器程序都运行在开发板或 Ubuntu 系统,这都是没问题的。
首先编译服务器应用程序和客户端应用程序:

图 29.4-1 编译客户端应用程序和服务器应用程序
编译得到 client 和 server 可执行文件,server 可执行文件在开发板上运行,client 可执行文件在 PC 端Ubuntu 系统下运行。将 server 可执行文件拷贝到开发板家目录下,如下所示:
图 29.4-2 将服务器执行文件拷贝到开发板


在开发板执行服务端:
图 29.4-3 先执行服务器应用程序


接着在 Ubuntu 系统执行客户端程序:
图 29.4-4 执行客户端应用程序


客户端运行之后将会去连接远端服务器,连接成功便会打印出信息“服务器连接成功...”,此时服务器也会监测到客户端连接,会打印相应的信息,如下所示:
图 29.4-5 服务器监测到客户端连接


接下来我们便可以在客户端处输入字符串,客户端程序会将我们输入的字符串信息发送给服务器,服务器接收到之后将其打印出来,如下所示:

图 29.4-6 输入字符串信息
图 29.4-7 服务器接收到客户端发送的信息


 


总结
到此,本章的内容就结束了,内容讲得非常浅,目的其实并不是让大家学会网络编程,这个是不可能的,旨在以引导大家入门为主,让大家对 socket 网络编程有一个基本的了解和认识。因为网络编程本就是应用编程中一门比较专业的方向,如果大家将来想从事这方面的工作、或者以后从事了这方面工作,再去找资料好好学习,如果没有这个打算,那就不要去深入研究这个,有了基本的了解、认识就行了。

posted @ 2025-12-18 11:08  前沿风暴  阅读(0)  评论(0)    收藏  举报