深入理解UNIX系统中的套接字网络编程
1. 套接字基础函数
在UNIX系统的网络编程中,套接字是核心概念。首先是
connect
函数,其原型如下:
#include <sys/socket.h>
int connect(int s, struct sockaddr *name, int addrlen);
该函数用于将由
s
引用的套接字连接到
name
所描述地址的服务器。
addrlen
参数指定
name
中地址的长度。若连接成功,
connect
返回0;否则返回 -1,并将失败原因存于
errno
中。客户端也可用此函数将数据报套接字连接到服务器,虽非严格必要,也不实际建立连接,但能让客户端在发送数据报时无需为每个数据报指定目标地址。
2. 数据传输
-
基于流的连接
:在基于流的连接中传输数据,客户端和服务器可直接使用
read和write函数。不过,还有两个专门用于基于流的套接字的函数:
#include <sys/types.h>
#include <sys/socket.h>
int recv(int s, char *buf, int len, int flags);
int send(int s, const char *buf, int len, int flags);
这两个函数与
read
和
write
基本相同,只是多了第四个参数
flags
,用于指定影响数据发送或接收方式的标志,具体标志及作用如下表:
| 标志 | 作用 |
| ---- | ---- |
| MSG_DONTROUTE | 在
send
调用中指定时,禁用数据的网络路由,仅用于诊断和路由程序 |
| MSG_OOB | 在
send
调用中指定时,数据作为带外数据发送,可“跳过”已发送但未接收的其他数据,如用于处理远程登录会话中的中断字符;在
recv
调用中指定时,返回任何待处理的带外数据而非“常规”数据 |
| MSG_PEEK | 在
recv
调用中指定时,数据像往常一样复制到
buf
中,但不会“消耗”,再次调用
recv
将返回相同数据,允许程序在读取数据前“窥视”,以决定如何处理 |
-
基于数据报的套接字
:使用基于数据报的套接字时,服务器不调用
listen或accept,客户端(通常)也不调用connect。因此,操作系统无法自动确定这些套接字上的数据要发送到哪里。为此,定义了另外两个函数:
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int s, char *buf, int len, int flags,
struct sockaddr *from, int *fromlen);
int sendto(int s, const char *buf, int len, int flags,
struct sockaddr *to, int tolen);
sendto
函数通过
s
引用的套接字将
buf
中的
len
字节数据发送到
to
指定地址的服务器,
tolen
指定地址长度,返回实际传输的字节数,出错返回 -1,且无法确定数据是否实际到达目的地。
recvfrom
函数从
s
引用的套接字接收最多
len
字节的数据并存储在
buf
中,数据来源地址存储在
from
中,
fromlen
会被修改以指示地址长度,返回接收的字节数,出错返回 -1。
3. 关闭通信通道
-
close函数 :可使用close函数关闭套接字,若套接字是基于流的,close会阻塞直到所有数据传输完毕。 -
shutdown函数 :也可使用shutdown函数关闭通信通道,原型如下:
#include <sys/types.h>
#include <sys/socket.h>
int shutdown(int s, int how);
该函数根据
how
的值关闭由
s
引用的通信通道的一侧或两侧。若
how
为0,套接字关闭读取功能,后续从套接字读取将返回文件结束符;若
how
为1,套接字关闭写入功能,后续向套接字写入将失败,同时告知操作系统无需再尝试发送套接字上的任何未发送数据;若
how
为2,套接字两侧都关闭,基本变得无用。
4. 示例代码
以下是几个示例代码,展示了如何使用上述函数进行网络编程。
4.1 服务器示例
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define PORTNUMBER 12345
int
main(void)
{
char buf[1024];
int n, s, ns, len;
struct sockaddr_in name;
/*
* Create the socket.
*/
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(1);
}
/*
* Create the address of the server.
*/
memset(&name, 0, sizeof(struct sockaddr_in));
name.sin_family = AF_INET;
name.sin_port = htons(PORTNUMBER);
len = sizeof(struct sockaddr_in);
/*
* Use the wildcard address.
*/
n = INADDR_ANY;
memcpy(&name.sin_addr, &n, sizeof(long));
/*
* Bind the socket to the address.
*/
if (bind(s, (struct sockaddr *) &name, len) < 0) {
perror("bind");
exit(1);
}
/*
* Listen for connections.
*/
if (listen(s, 5) < 0) {
perror("listen");
exit(1);
}
/*
* Accept a connection.
*/
if ((ns = accept(s, (struct sockaddr *) &name, &len)) < 0) {
perror("accept");
exit(1);
}
/*
* Read from the socket until end-of-file and
* print what we get on the standard output.
*/
while ((n = recv(ns, buf, sizeof(buf), 0)) > 0)
write(1, buf, n);
close(ns);
close(s);
exit(0);
}
该服务器程序的工作流程如下:
1. 创建套接字。
2. 创建服务器地址。
3. 将套接字绑定到地址。
4. 监听连接。
5. 接受连接。
6. 从套接字读取数据并打印到标准输出。
7. 关闭套接字。
4.2 客户端示例
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <netdb.h>
#include <stdio.h>
#define PORTNUMBER 12345
int
main(void)
{
int n, s, len;
char buf[1024];
char hostname[64];
struct hostent *hp;
struct sockaddr_in name;
/*
* Get our local host name.
*/
if (gethostname(hostname, sizeof(hostname)) < 0) {
perror("gethostname");
exit(1);
}
/*
* Look up our host's network address.
*/
if ((hp = gethostbyname(hostname)) == NULL) {
fprintf(stderr, "unknown host: %s.\n", hostname);
exit(1);
}
/*
* Create a socket in the INET
* domain.
*/
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(1);
}
/*
* Create the address of the server.
*/
memset(&name, 0, sizeof(struct sockaddr_in));
name.sin_family = AF_INET;
name.sin_port = htons(PORTNUMBER);
memcpy(&name.sin_addr, hp->h_addr_list[0], hp->h_length);
len = sizeof(struct sockaddr_in);
/*
* Connect to the server.
*/
if (connect(s, (struct sockaddr *) &name, len) < 0) {
perror("connect");
exit(1);
}
/*
* Read from standard input, and copy the
* data to the socket.
*/
while ((n = read(0, buf, sizeof(buf))) > 0) {
if (send(s, buf, n, 0) < 0) {
perror("send");
exit(1);
}
}
close(s);
exit(0);
}
该客户端程序的工作流程如下:
1. 获取本地主机名。
2. 查找主机的网络地址。
3. 创建套接字。
4. 创建服务器地址。
5. 连接到服务器。
6. 从标准输入读取数据并发送到套接字。
7. 关闭套接字。
4.3 数据报客户端示例
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <netdb.h>
#include <stdio.h>
#define SERVICENAME "daytime"
int
main(int argc, char **argv)
{
int n, s, len;
char buf[1024];
char *hostname;
struct hostent *hp;
struct servent *sp;
struct sockaddr_in name, from;
if (argc < 2) {
fprintf(stderr, "Usage: %s hostname [hostname...]\n", *argv);
exit(1);
}
/*
* Look up our service. We want the UDP version.
*/
if ((sp = getservbyname(SERVICENAME, "udp")) == NULL) {
fprintf(stderr, "%s/udp: unknown service.\n", SERVICENAME);
exit(1);
}
while (--argc) {
hostname = *++argv;
/*
* Look up the host's network address.
*/
if ((hp = gethostbyname(hostname)) == NULL) {
fprintf(stderr, "%s: unknown host.\n", hostname);
continue;
}
/*
* Create a socket in the INET
* domain.
*/
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket");
exit(1);
}
/*
* Create the address of the server.
*/
memset(&name, 0, sizeof(struct sockaddr_in));
name.sin_family = AF_INET;
name.sin_port = sp->s_port;
memcpy(&name.sin_addr, hp->h_addr_list[0], hp->h_length);
len = sizeof(struct sockaddr_in);
/*
* Send a packet to the server.
*/
memset(buf, 0, sizeof(buf));
n = sendto(s, buf, sizeof(buf), 0, (struct sockaddr *) &name,
sizeof(struct sockaddr_in));
if (n < 0) {
perror("sendto");
exit(1);
}
/*
* Receive a packet back.
*/
len = sizeof(struct sockaddr_in);
n = recvfrom(s, buf, sizeof(buf), 0, (struct sockaddr *) &from, &len);
if (n < 0) {
perror("recvfrom");
exit(1);
}
/*
* Print the packet.
*/
buf[n] = '\0';
printf("%s: %s", hostname, buf);
/*
* Close the socket.
*/
close(s);
}
exit(0);
}
该数据报客户端程序的工作流程如下:
1. 检查命令行参数是否足够。
2. 查找服务的UDP版本。
3. 遍历每个主机名:
- 查找主机的网络地址。
- 创建套接字。
- 创建服务器地址。
- 向服务器发送数据包。
- 接收服务器返回的数据包。
- 打印数据包内容。
- 关闭套接字。
5. 其他函数
除了上述常用函数,还有一些不太常用但仍很有用的函数。
5.1 获取套接字名称
#include <sys/types.h>
#include <sys/socket.h>
int getsockname(int s, struct sockaddr *name, int *namelen);
int getpeername(int s, struct sockaddr *name, int *namelen);
getsockname
函数获取绑定到套接字
s
的名称并存储在
name
指向的区域,
namelen
存储名称的长度,调用前应初始化为
name
指向区域的大小,返回时将设置为名称的实际长度。
getpeername
函数获取连接到套接字
s
的对等方的名称,即远程主机的地址和端口号,服务器可借此了解谁连接到了它。两个函数成功时返回0,失败时返回 -1 并将错误代码存储在
errno
中。
5.2 套接字选项
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int s, int level, int optname, char *optval, int *optlen);
int setsockopt(int s, int level, int optname, char *optval, int optlen);
getsockopt
函数返回当前设置在套接字
s
上的选项状态信息,
setsockopt
函数更改这些选项的状态。选项可能存在于多个协议级别,这里所有选项都在套接字级别,
level
参数应始终设置为
SOL_SOCKET
。
optval
参数指定一个指向缓冲区的指针,该缓冲区要么包含要为选项设置的值,要么用于存储选项的值。
optlen
参数指定
optval
指向区域的大小,从
getsockopt
返回时,
optlen
将被修改以指示值的实际大小。
optname
参数指定感兴趣的选项,常见选项及作用如下表:
| 选项 | 作用 |
| ---- | ---- |
| SO_DEBUG | 启用或禁用底层协议模块的调试功能 |
| SO_REUSEADDR | 指示修改
bind
调用中验证地址的规则,以允许重用本地地址 |
| SO_KEEPALIVE | 启用在已连接套接字上定期发送“你在吗”消息的功能,若连接方未响应,连接将被视为断开,使用该套接字的进程下次尝试使用时将收到
SIGPIPE
信号 |
| SO_DONTROUTE | 指示传出消息应绕过网络路由设施,仅用于调试和诊断目的 |
| SO_LINGER | 若在保证可靠数据传输的套接字上设置该选项,关闭套接字时,系统将阻塞进程,直到所有未发送的数据传输完毕或传输超时,超时时间(秒)在
setsockopt
的
optval
参数中指定;若禁用该选项,关闭操作将以允许调用进程尽快继续的方式处理 |
| SO_BROADCAST | 请求允许在套接字上发送广播数据报(将被所有主机接收的数据报) |
| SO_OOBINLINE | 在支持带外数据的套接字上,请求将带外数据到达时放入正常输入队列,允许使用不带
MSG_OOB
标志的
read
或
recv
调用处理数据 |
| SO_SNDBUF, SO_RCVBUF | 分别调整正常发送和接收缓冲区的大小,一般来说,对于大数据传输,应尽量增大这些缓冲区以提高传输效率,SVR4 中缓冲区大小的最大限制为 64 Kbytes |
| SO_TYPE | 仅用于
getsockopt
,返回套接字的类型(如
SOCK_STREAM
) |
| SO_ERROR | 仅用于
getsockopt
,返回套接字上的任何待处理错误并清除错误状态 |
5.3 地址转换
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_addr(const char *cp);
char *inet_ntoa(const struct in_addr addr);
inet_addr
函数接受一个包含“点分十进制”表示的互联网地址的字符串,并返回该地址的整数表示。
inet_ntoa
函数接受一个互联网地址的整数表示,并返回该地址的“点分十进制”字符串表示。
6. 伯克利“R”命令
-
rcmd函数 :伯克利rsh命令的功能可通过rcmd函数实现,原型如下:
int rcmd(char **ahost, unsigned short inport, char *luser,
char *ruser, char *cmd, int *fd2p);
该函数使用
inport
指定的保留端口连接到
*aname
指定的主机,成功时返回一个流套接字,失败时返回 -1。
luser
参数应包含本地用户的名称,
ruser
参数应包含远程主机上用于执行命令的用户的名称。在远程主机上,
rshd
守护进程将在
ruser
的
.rhosts
文件中查找指定连接主机和
luser
的行,若找到则授予访问权限,否则拒绝访问。若访问被授予,将执行
cmd
中的 shell 命令,命令的标准输入和输出将连接到
rcmd
返回的套接字。若
fd2p
不为空,将设置一个到控制进程的辅助通道,并将其描述符放入
*fd2p
,控制进程将在该通道上返回命令的标准错误输出,也会接受该通道上的字节作为要发送到命令进程组的信号编号;若
fd2p
为空,命令的标准错误输出将与标准输出相同,且不会提供向进程发送信号的功能。
-
rresvport
函数
:用于获取保留端口号,原型如下:
int rresvport(int *port);
它返回一个适合用作
rcmd
的
inport
参数的保留端口,出错时返回 -1。
-
rexec
函数
:为避免
rcmd
需要保留端口的问题,有
rexec
函数,原型如下:
int rexec(char **ahost, unsigned short inport, char *user,
char *password, char *cmd, int *fd2p);
其用法和参数与
rcmd
基本相同,但
inport
参数不必指定保留端口,且需要指定远程主机的登录名和密码进行认证。不过,由于需要密码,使用
rexec
的程序在非交互环境下使用不安全。
-
ruserok
函数
:服务器可通过调用
ruserok
函数实现基于
.rhosts
的认证,原型如下:
int ruserok(char *rhost, int suser, char *ruser, char *luser);
rhost
参数应为
gethostbyaddr
返回的远程主机名,
ruser
参数是远程主机上调用用户的名称,
luser
参数是本地主机上的用户名称(应检查其
.rhosts
文件)。若
luser
是超级用户,
suser
标志应设为1,否则设为0,这将绕过对
/etc/hosts.equiv
文件的检查。
7. inetd 超级服务器
最初,伯克利开发网络支持时,每个服务由单独的守护进程服务器处理。随着服务数量增加,守护进程数量也增多,但很多守护进程很少执行,一直占用系统资源且使进程表混乱。为解决此问题,创建了
inetd
程序。
inetd
是超级服务器,它读取配置文件(通常是
/etc/inetd.conf
),为文件中列出的每个服务打开一个套接字并绑定到相应端口。当有连接或数据报到达这些端口之一时,
inetd
会派生一个子进程并执行负责处理该服务的守护进程。这样,大部分时间只有
inetd
在运行,其他守护进程仅在有任务时运行,节省了系统资源。
当通过
inetd
调用守护进程服务器时,其标准输入和输出连接到套接字,服务器从标准输入读取实际是从网络读取,向标准输出写入实际是向网络写入,之前提到的
socket
、
bind
、
accept
和
listen
调用都不再需要。若服务器需要知道谁(哪个主机)正在连接,可使用
getpeername
函数。一般来说,服务器应设计为通过
inetd
运行,这样通常更高效且简单,唯一的例外是接收大量连接的服务器,因为
inetd
为每个连接派生新服务器副本的性能开销可能超过不一直运行另一个服务器所节省的性能。
8. 总结
网络编程在 UNIX 系统中是一个重要且相对简单的领域。通过使用套接字相关的函数,我们可以方便地实现客户端和服务器之间的数据传输。同时,了解其他相关函数和概念,如获取套接字名称、设置套接字选项、地址转换、伯克利“R”命令和
inetd
超级服务器等,能让我们更深入地掌握 UNIX 网络编程的技巧和方法。如果想进一步了解 UNIX 网络编程,可以查看一些实际的网络程序代码,如
ping
、
tftp
和
rlogin
等,这些代码能帮助我们更好地理解各个部分是如何组合在一起的。此外,互联网上有很多相关的操作系统源代码可供参考,如伯克利 4.4BSD Lite、Linux、386BSD 和 FreeBSD 等,也可以阅读专业书籍来深入学习。
深入理解UNIX系统中的套接字网络编程
9. 套接字编程的优势与应用场景
-
优势
- 可移植性 :套接字编程范式在UNIX系统中广泛应用,并且大多数UNIX版本的供应商都采用了伯克利的实现,因此具有很高的可移植性。这意味着开发者可以在不同的UNIX系统上使用相同的代码进行网络编程,减少了开发和维护的成本。
- 灵活性 :通过使用不同的套接字类型(如流套接字和数据报套接字)和函数,可以实现各种不同的网络应用,从简单的客户端 - 服务器通信到复杂的分布式系统。
- 高效性 :套接字编程提供了底层的网络接口,允许开发者直接控制数据的传输,从而实现高效的网络通信。
-
应用场景
- 文件传输 :如FTP(文件传输协议),通过套接字编程可以实现文件的上传和下载。
- 远程登录 :像Telnet和SSH,允许用户远程登录到其他计算机并执行命令。
- 实时通信 :例如聊天程序,通过套接字可以实现实时的消息传递。
10. 网络编程中的错误处理
在网络编程中,错误处理是非常重要的。因为网络环境复杂多变,可能会出现各种错误,如连接失败、数据传输错误等。以下是一些常见的错误处理方法:
-
使用
errno
:大多数网络函数在出错时会设置全局变量
errno
,可以通过检查
errno
的值来确定错误的原因。例如,在
connect
函数调用失败后,可以通过检查
errno
来了解是网络不可达还是服务器拒绝连接等。
-
perror
函数
:可以使用
perror
函数输出错误信息,它会根据
errno
的值输出相应的错误描述。例如:
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(1);
}
- 自定义错误处理函数 :可以编写自定义的错误处理函数,根据不同的错误情况进行不同的处理。例如:
void handle_error(const char *msg) {
perror(msg);
exit(1);
}
然后在需要的地方调用该函数:
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
handle_error("socket");
}
11. 网络编程的性能优化
为了提高网络编程的性能,可以采取以下一些优化措施:
-
缓冲区调整
:通过设置
SO_SNDBUF
和
SO_RCVBUF
选项来调整发送和接收缓冲区的大小。对于大数据传输,增大缓冲区可以减少数据传输的次数,提高传输效率。例如:
int sndbuf_size = 64 * 1024; // 64 Kbytes
if (setsockopt(s, SOL_SOCKET, SO_SNDBUF, &sndbuf_size, sizeof(sndbuf_size)) < 0) {
perror("setsockopt");
}
-
异步I/O
:使用异步I/O模型可以避免在等待数据传输时阻塞线程,提高程序的并发性能。例如,可以使用
select、poll或epoll等函数来实现异步I/O。 - 连接复用 :对于频繁建立和断开连接的应用,可以考虑复用已有的连接,减少连接建立和断开的开销。
12. 网络编程的安全问题
网络编程中需要考虑安全问题,以防止数据泄露和恶意攻击。以下是一些常见的安全措施:
-
认证
:使用用户名和密码进行认证,如
rexec
函数中需要指定远程主机的登录名和密码。
-
加密
:对传输的数据进行加密,防止数据在传输过程中被窃取。可以使用SSL/TLS协议来实现数据加密。
-
防火墙
:设置防火墙来限制网络访问,只允许特定的IP地址或端口进行通信。
13. 总结与展望
网络编程在UNIX系统中具有重要的地位,通过套接字编程可以实现各种网络应用。本文介绍了套接字编程的基础函数、数据传输、关闭通信通道、示例代码以及其他相关函数和概念,同时还讨论了网络编程的优势、应用场景、错误处理、性能优化和安全问题。
未来,随着网络技术的不断发展,网络编程也将面临新的挑战和机遇。例如,随着物联网的兴起,需要处理更多的设备连接和数据传输;随着云计算的发展,需要实现更高效的分布式系统。因此,开发者需要不断学习和掌握新的网络编程技术,以适应不断变化的需求。
同时,网络安全问题也将变得越来越重要。开发者需要加强对网络安全的认识,采取有效的安全措施来保护数据的安全。
总之,网络编程是一个充满挑战和机遇的领域,通过不断学习和实践,开发者可以掌握更多的网络编程技巧,开发出更加高效、安全的网络应用。
14. 参考代码流程总结
为了更好地理解上述内容,下面通过mermaid流程图总结服务器和客户端示例代码的流程。
graph TD;
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(创建套接字):::process;
B --> C(创建服务器地址):::process;
C --> D(绑定套接字到地址):::process;
D --> E(监听连接):::process;
E --> F{是否有连接请求}:::decision;
F -- 是 --> G(接受连接):::process;
G --> H(从套接字读取数据):::process;
H --> I(打印数据到标准输出):::process;
I --> J{是否还有数据}:::decision;
J -- 是 --> H;
J -- 否 --> K(关闭套接字):::process;
K --> L([结束]):::startend;
F -- 否 --> F;
图1:服务器示例代码流程
graph TD;
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(获取本地主机名):::process;
B --> C(查找主机的网络地址):::process;
C --> D(创建套接字):::process;
D --> E(创建服务器地址):::process;
E --> F(连接到服务器):::process;
F --> G{是否连接成功}:::decision;
G -- 是 --> H(从标准输入读取数据):::process;
H --> I(发送数据到套接字):::process;
I --> J{是否还有数据}:::decision;
J -- 是 --> H;
J -- 否 --> K(关闭套接字):::process;
K --> L([结束]):::startend;
G -- 否 --> M(处理连接失败):::process;
M --> L;
图2:客户端示例代码流程
通过以上流程图,可以更清晰地看到服务器和客户端程序的执行流程,有助于开发者更好地理解和调试代码。
超级会员免费看
975

被折叠的 条评论
为什么被折叠?



