深入理解Linux Socket系统调用 (linux socket系统调用)

Linux操作系统广泛应用于服务器、互联网、嵌入式设备等领域,其成功的原因之一便是其强大的网络支持。Linux提供了一套完整的Socket编程API,使得程序员可以方便地实现网络应用程序。Socket编程是网络编程的基础,对于开发网络应用程序至关重要。

本文将系统地介绍Linux Socket系统调用相关知识,包括Socket概念、Socket编程模型、Socket系统调用接口、Socket编程应用例程等,并结合应用场景进行实践演示,以期读者能够,掌握Socket编程技术。

一、Socket概念

Socket是一种传输层协议,用于实现进程间的网络通信,是Linux中程序进行网络通信的主要手段。Socket提供了一套完整的API,使得程序员能够方便地进行网络编程。

在网络编程中,Socket常用于描述生成的套接字(socket),套接字是使用网络传输协议进行通信的两个程序之间的一条双向通信链路。套接字可使用网络传输协议TCP、UDP、RAW等。

二、Socket编程模型

Socket编程模型是一种典型的客户端/服务器模型,其中服务器程序和客户端程序分别运行在不同的主机上,通过对套接字API的调用来实现进程间的通信。Socket编程模型通常包括三个要素:服务器、客户端和Socket。

1、服务器

服务器是Socket编程模型中的核心角色,其职责是等待客户端的连接请求,并对连接请求进行处理。服务器通过Socket系统调用创建一个用于侦听连接请求的套接字,并通过accept系统调用对连接请求进行响应,最后通过接受到的客户端请求来完成进程间的通信。

2、客户端

客户端是Socket编程模型中的请求方,其职责是主动与服务器建立Socket连接,并通过Socket传输数据。客户端通过Socket系统调用创建一个用于连接服务器的套接字,并通过connect系统调用与服务器建立连接。建立连接后,客户端可以使用send和recv系统调用来发送和接受数据。

3、Socket

Socket是Socket编程模型中进程间通信的主要手段,其实现方式与文件读写类似,可以使用系统调用对其进行读写操作。对于服务器端,Socket主要用来侦听客户端连接请求,创建客户端与服务器之间的连接;对于客户端,Socket主要用来主动发起连接请求,并完成数据传输操作。

三、Socket系统调用

Socket系统调用是Linux操作系统提供的一套API接口,用于操作Socket。Socket系统调用为Socket编程提供了多种API,包括创建Socket、建立连接、发送数据、接收数据等。

以下是常用的Socket系统调用:

1、socket:创建一个新的Socket,返回套接字描述符。

2、bind:将地址和端口号绑定到Socket上。

3、listen:开始监听来自客户端的连接请求。

4、accept:接收客户端的连接请求,返回一个新的套接字描述符。

5、connect:主动与服务器建立连接。

6、send:向已连接的Socket中发送数据。

7、recv:从已连接的Socket中接收数据。

8、shutdown:关闭Socket的发送或接收通道。

四、Socket编程应用例程

下面通过一个简单的Socket编程应用例子来说明Socket编程的具体应用。

使用Socket编写一个简单的服务器来接受客户端的输入,并将其发送回客户端。

1、服务器端程序演示代码:

“`

#include

#include

#include

#include

#include

#include

#include

int mn(void)

{

int server_fd, client_fd;

socklen_t client_len;

struct sockaddr_in server_addr, client_addr;

char buffer[1024];

memset(&server_addr, 0, sizeof(server_addr));

server_addr.sin_family = AF_INET;

server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

server_addr.sin_port = htons(30001);

if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

perror(“server: socket error”);

exit(1);

}

if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {

perror(“server: bind error”);

exit(1);

}

if (listen(server_fd, 5) == -1) {

perror(“server: listen error”);

exit(1);

}

printf(“server wting for client on port 30001…\n”);

while (1) {

client_len = sizeof(client_addr);

client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);

if (client_fd == -1) {

perror(“server: accept error”);

continue;

}

printf(“server: connection from client %s\n”, inet_ntoa(client_addr.sin_addr));

while (1) {

memset(buffer, 0, sizeof(buffer));

if (recv(client_fd, buffer, sizeof(buffer), 0) == -1) {

perror(“server: receive error”);

break;

}

if (strcmp(buffer, “quit\n”) == 0) {

printf(“server: client %s quits\n”, inet_ntoa(client_addr.sin_addr));

break;

}

printf(“server: receive message %s”, buffer);

if (send(client_fd, buffer, strlen(buffer), 0) == -1) {

perror(“server: send error”);

break;

}

}

close(client_fd);

}

close(server_fd);

return 0;

}

“`

2、客户端程序演示代码:

“`

#include

#include

#include

#include

#include

#include

#include

#include

int mn(int argc, char *argv[])

{

int client_fd, numbytes;

struct hostent *he;

struct sockaddr_in server_addr;

char message[1024];

if (argc != 2) {

fprintf(stderr, “usage: client hostname\n”);

exit(1);

}

if ((he = gethostbyname(argv[1])) == NULL) {

perror(“client: gethostbyname error”);

exit(1);

}

if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

perror(“client: socket error”);

exit(1);

}

memset(&server_addr, 0, sizeof(server_addr));

server_addr.sin_family = AF_INET;

server_addr.sin_addr = *((struct in_addr*)he->h_addr);

server_addr.sin_port = htons(30001);

if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {

perror(“client: connect error”);

exit(1);

}

printf(“client: connected to server %s\n”, argv[1]);

while (1) {

memset(message, 0, sizeof(message));

fgets(message, sizeof(message), stdin);

if (strcmp(message, “quit\n”) == 0) {

printf(“client quits\n”);

break;

}

if (send(client_fd, message, strlen(message), 0) == -1) {

perror(“client: send error”);

exit(1);

}

if ((numbytes = recv(client_fd, message, sizeof(message), 0)) == -1) {

perror(“client: recv error”);

exit(1);

}

message[numbytes] = ‘\0’;

printf(“client: receive message %s”, message);

}

close(client_fd);

return 0;

}

“`

运行结果:

1、服务器端运行结果

“`

server wting for client on port 30001…

server: connection from client 127.0.0.1

server: receive message abc

server: receive message def

server: client 127.0.0.1 quits

“`

2、客户端运行结果

“`

client: connected to server 127.0.0.1

abc

client: receive message abc

def

client: receive message def

quit

client quits

“`

以上代码演示了一个简单的Socket编程应用,该应用实现了客户端和服务器端的互相通信,可以作为Socket编程的入门代码进行学习和研究。

相关问题拓展阅读:

linux手册翻译——send(2)

send, sendto, sendmsg – send a message on a socket

系统调用 send()、sendto() 和 sendmsg() 用于将消息传输到另一个套接字。

仅当套接字处于连接状态时才可以使用 send() 调用(以便知道预期的接收者, 也就是说send()仅仅用于数据流类型的数据发送 ,对于TCP,服务端和客户端都可以使用send/recv;但是对于UDP,只能是客户端使用send/recv,服务端只能使用sendto/recvfrom,因为客户端是进行了connect操作知道要发送和接受的地址)。send() 和 write(2) 之间的唯一渗衡区别是存在 flags 参数。此外,

send(sockfd, buf, len, flags);

等价于

sendto(sockfd, buf, len, flags, NULL, 0);

参数 sockfd 是发送者套接字的文件描述符。

如果在连接模式的套接字(即套接字类型为SOCK_STREAM、SOCK_SEQPACKET)上使用 sendto(),则参数 dest_addr 和 addrlen 将被忽略(当它们不是NULL和0时可能返回错误EISCONN),若套接字没有实际连接(还没有三次握手建立连接)将返回错误ENOTCONN。 否则,目标地址由 dest_addr 给出, addrlen 指定其大小。 对于 sendmsg(),目标地址由 msg.msg_name 给出, msg.msg_namelen 指定其大小。

对于 send() 和 sendto(),消息位于 buf 中,长度为 len 。 对于sendmsg(),消息存放于 msg.msg_iov 元素指向

数组数据区

(见下)中。

sendmsg() 调用还允许发送辅助数据(也称为控制信息)

如果消息太长而无法通过底层协议原子传递( too long to pass atomically through the underlying protocol ),则返回错误 EMSGSIZE,并且不会传输消息。

No indication of failure to deliver is implicit in a send(). Locally detected errors are indicated by a return value of -1.

当消息轿陵不适合套接字的发送缓冲区时,send() 通常会阻塞,除非套接字已置于非阻塞 I/O 模式。 在这种情况下,在非阻塞模式下它会失败并显示错误 EAGAIN 或 EWOULDBLOCK。

select(2) 调用可用于确定何时可以发送更多数据

上面的的描述还是很笼统的,以TCP为例,按我的理解,我认为只要发送缓冲区有空闲位置,且此时协议栈没有向网络发送数据,那么就可以写入,对于阻塞模式,直到所有数据写入到缓冲区,就会返回,否则一直阻塞,对于非阻塞模式,是有一个超时时间的,这个由 SO_SNDTIMEO 选项控制,详细见 socket(7) ,如果当前有空闲位置可以发即当前可写入,那么就写入到缓冲区,知道超时之前写入多少算多少,然后返回成功写入的字节数,如果超时时任何数据都没写出去,或者当前就是闭喊戚不可写入,那么返回-1 ,并设置errno为 EAGAIN 或 EWOULDBLOCK。

The flags argument is the bitwise OR of zero or more of the following flags.

sendmsg() 使用的 msghdr 结构的定义如下:

对于未连接的套接字 msg_name 指定数据报的目标地址,它指向一个包含地址的缓冲区; msg_namelen 字段应设置为地址的大小。 对于连接的套接字,这些字段应分别指定为 NULL 和 0。

这里的未连接指的是数据报协议,连接指的是数据流协议

The msg_iov and msg_iovlen fields specify scatter-gather locations, as for writev(2).

msg_iov是一个buffer数组:

使用 msg_control 和 msg_controllen 成员发送控制信息(辅助数据)。 内核可以处理的每个套接字更大控制缓冲区长度由 /proc/sys/net/core/optmem_max 中的值限制; 见 socket(7) 。 有关在各种套接字域中使用辅助数据的更多信息,请参阅 unix(7) 和 ip(7)。

msg_flags 字段被忽略。

成功时,返回成功发送的字节数,这个字节数并不一定和我们的缓冲区大小相同

。 出错时,返回 -1,并设置 errno 以指示错误。

这些是套接字层生成的一些标准错误。 底层协议模块可能会产生和返回额外的错误; 请参阅它们各自的手册页。

4.4BSD, SVr4, POSIX.1-2023. These interfaces first appeared in 4.2BSD.

POSIX.describes only the MSG_OOB and MSG_EOR flags. POSIX.adds a specification of MSG_NOSIGNAL. The MSG_CONFIRM flag is a Linux extension.

根据 POSIX.1-2023,msghdr 结构的 msg_controllen 字段应该是 socklen_t 类型,而 msg_iovlen 字段应该是 int 类型,但是 glibc 目前将两者都视为 size_t。

有关可用于在单个调用中传输多个数据报的 Linux 特定系统调用的信息,请参阅 sendmmsg(2)。

Linux may return EPIPE instead of ENOTCONN.

getaddrinfo(3) 中显示了使用 send() 的示例。

Linux系统I/O模型及select、poll、epoll原理和应用

理解Linux的IO模型之前,首先要了解一些基本概念,才能理解这些IO模型设计的依据

操作系统使用虚拟内旦谈磨存来映射物理内存,对于32位的操作系统来说,虚拟地址空间为4G(2^32)。操作系统的核心是内核,为了保护用户进程不能直接操作内核,保证内核安全,操作系统将虚拟地址空间划分为内核空间和用户空间。内核可以访问全部的地址空间,拥有访问底层硬件设备的权限,普通的应用程序需要访问硬件设备必须通过

系统调用

来实现。

对于Linux系统来说,将虚拟内存的更高1G字节的空间作为内核空间仅供内核使用,低3G字节的空间供用户进程使用,称为用户空间。

又被称为标准I/O,大多数文件系统的默认I/O都是缓存I/O。在Linux系统的缓存I/O机制中,操作系统会将I/O的数据缓存在页缓存(内存)中,也就是数据先被拷贝到内核的缓冲区(内核地址空间),然后才会从内核缓冲区拷贝到应用程序的缓冲区(用户地址空间)。

这种方式很明显的缺点就是数据传输过程中需要再应用程序地址空间和内核空间进行多次数据拷贝操作,这些操作带来的CPU以及内存的开销是非常大的。

由于Linux系统采用的缓存I/O模式,对于一次I/O访问,以读操作举例,数据先会被拷贝到内核缓冲区,然后才会从内核缓冲区拷贝到应用程序的缓存区,当一个read系统调用发生的时候,会经历两个阶段:

正是因为这两个状态,Linux系统才产生了多种不同的网络I/O模式的方案

Linux系统默认情况下所有socke都是blocking的,一个读操作流程如下:

以UDP socket为例,当用户进程调用了recvfrom系统调用,如果数据还没准备好,应用进程被阻塞,内核直到数据到来且将数据从内核缓冲区拷贝到了应用进程缓冲区,然后向用户进程返回结果,用户进程才解除block状态,重新运行起来。

阻塞模行下只是阻塞了当前的应用进程,其他进程还可以执行,不消耗CPU时间,CPU的利用率较高。

Linux可以设置socket为非阻塞的,非阻塞模式下执行一个读操作流程如下:

当用户进程发出recvfrom系统调用时,如果kernel中的数据还没准备好,模斗recvfrom会立即返回一个error结果,不会阻塞用户进程,用户进程收到error时知道数据还没准备好,过一会再调用recvfrom,直到kernel中的数据准备好了,内核就立即将数据拷贝到用户内存然后返回ok,这个过程需要用户进程去轮询内核数据是否准备好。

非阻塞模型下由于要处理更多的系统调用,因此CPU利用率比较低。

应用进程使用sigaction系统调用,内核立即返回,等到kernel数据准备好时会给用户进程发送一个信号,告诉用户进程可以进行IO操作了,然后用户进程再调用IO系统调用如recvfrom,将数据从内核缓冲区拷贝到应用进程。流程如下:

相比于轮询的方式,不需要多次系统调用轮询,信号驱动IO的CPU利用率更高。

异步IO模型与其他模型更大的区别是,异步IO在系统调用返回的时候所有操作都已经完成,应用进程既不需要等待数据准备,也不需要在数据到来后等待数据从内核缓冲区拷贝到用户缓冲区,流程如下:

在数据拷贝完成后,kernel会给用户进程发送一个信号告诉其read操作完成了。

是用select、poll等待数据,可以等待多个socket中的任一个变为可读,这一过程会被阻塞,当某个套接字数据到来时返回,之后再用recvfrom系统调用把数据从内核缓存区复制到用户进程,流程如下:

流程类似阻塞IO,甚至比阻塞IO更差,多使用了一个系统调用,但是IO多路复用更大的侍兄特点是让单个进程能同时处理多个IO事件的能力,又被称为事件驱动IO,相比于多线程模型,IO复用模型不需要线程的创建、切换、销毁,系统开销更小,适合高并发的场景。

select是IO多路复用模型的一种实现,当select函数返回后可以通过轮询fdset来找到就绪的socket。

优点是几乎所有平台都支持,缺点在于能够监听的fd数量有限,Linux系统上一般为1024,是写死在宏定义中的,要修改需要重新编译内核。而且每次都要把所有的fd在用户空间和内核空间拷贝,这个操作是比较耗时的。

poll和select基本相同,不同的是poll没有更大fd数量限制(实际也会受到物理资源的限制,因为系统的fd数量是有限的),而且提供了更多的时间类型。

总结:select和poll都需要在返回后通过轮询的方式检查就绪的socket,事实上同时连的大量socket在一个时刻只有很少的处于就绪状态,因此随着监视的描述符数量的变多,其性能也会逐渐下降。

epoll是select和poll的改进版本,更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll_create()用来创建一个epoll句柄。

epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个就绪链表中管理。

epoll_wait() 可以从就绪链表中得到事件完成的描述符,因此进程不需要通过轮询来获得事件完成的描述符。

当epoll_wait检测到描述符IO事件发生并且通知给应用程序时,应用程序可以不立即处理该事件,下次调用epoll_wait还会再次通知该事件,支持block和nonblocking socket。

当epoll_wait检测到描述符IO事件发生并且通知给应用程序时,应用程序需要立即处理该事件,如果不立即处理,下次调用epoll_wait不会再次通知该事件。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用nonblocking socket,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

【segmentfault】 Linux IO模式及 select、poll、epoll详解

【GitHub】 CyC2023/CS-Notes

关于linux socket系统调用的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。


数据运维技术 » 深入理解Linux Socket系统调用 (linux socket系统调用)