View Javadoc
1   /*
2    * Copyright 2025 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.channel.uring;
17  
18  import io.netty.channel.Channel;
19  import io.netty.channel.ChannelFuture;
20  import io.netty.channel.ChannelFutureListener;
21  import io.netty.channel.ChannelOutboundBuffer;
22  import io.netty.channel.ChannelPipeline;
23  import io.netty.channel.ChannelPromise;
24  import io.netty.channel.IoRegistration;
25  import io.netty.channel.unix.DomainSocketAddress;
26  import io.netty.channel.unix.DomainSocketChannel;
27  import io.netty.channel.unix.DomainSocketChannelConfig;
28  import io.netty.channel.unix.DomainSocketReadMode;
29  import io.netty.channel.unix.Errors;
30  import io.netty.channel.unix.FileDescriptor;
31  import io.netty.channel.unix.PeerCredentials;
32  
33  import java.io.IOException;
34  import java.net.SocketAddress;
35  
36  /**
37   * {@link DomainSocketChannel} implementation that uses linux io_uring
38   */
39  public final class IoUringDomainSocketChannel extends AbstractIoUringStreamChannel implements DomainSocketChannel {
40  
41      private final IoUringDomainSocketChannelConfig config;
42  
43      private volatile DomainSocketAddress local;
44      private volatile DomainSocketAddress remote;
45  
46      public IoUringDomainSocketChannel() {
47          super(null, LinuxSocket.newSocketDomain(), false);
48          config = new IoUringDomainSocketChannelConfig(this);
49      }
50  
51      IoUringDomainSocketChannel(Channel parent, FileDescriptor fd) {
52          this(parent, new LinuxSocket(fd.intValue()));
53      }
54  
55      IoUringDomainSocketChannel(Channel parent, LinuxSocket fd) {
56          super(parent, fd, true);
57          local = fd.localDomainSocketAddress();
58          remote = fd.remoteDomainSocketAddress();
59          config = new IoUringDomainSocketChannelConfig(this);
60      }
61  
62      @Override
63      public DomainSocketChannelConfig config() {
64          return config;
65      }
66  
67      @Override
68      public DomainSocketAddress localAddress() {
69          return local;
70      }
71  
72      @Override
73      public DomainSocketAddress remoteAddress() {
74          return remote;
75      }
76  
77      /**
78       * Returns the unix credentials (uid, gid, pid) of the peer
79       * <a href=https://man7.org/linux/man-pages/man7/socket.7.html>SO_PEERCRED</a>
80       */
81      public PeerCredentials peerCredentials() throws IOException {
82          return socket.getPeerCredentials();
83      }
84  
85      @Override
86      protected Object filterOutboundMessage(Object msg) {
87          if (msg instanceof FileDescriptor) {
88              return msg;
89          }
90          return super.filterOutboundMessage(msg);
91      }
92  
93      @Override
94      protected AbstractUringUnsafe newUnsafe() {
95          return new IoUringDomainSocketUnsafe();
96      }
97  
98      @Override
99      protected boolean allowMultiShotPollIn() {
100         // UNIX domain sockets do not support IORING_CQE_F_SOCK_NONEMPTY and POLL_ADD_MULTI is edge-triggered
101         // so we should disable it
102         return false;
103     }
104 
105     @Override
106     protected boolean socketIsEmpty(int flags) {
107         return IoUring.isUnixDomainSocketInqSupported() && super.socketIsEmpty(flags);
108     }
109 
110     @Override
111     protected boolean shouldCompleteReadLoop(int flags, boolean multishot) {
112         if (IoUring.isUnixDomainSocketInqSupported()) {
113             return socketIsEmpty(flags);
114         }
115         // Older kernels cannot report IORING_CQE_F_SOCK_NONEMPTY for UDS, so the read-loop boundary cannot be
116         // determined reliably.
117         // Multishot recv does not produce an EAGAIN completion while it remains armed, so
118         // complete the read loop for each multishot completion. A one-shot recv can continue until EAGAIN.
119         return multishot;
120     }
121 
122     private final class IoUringDomainSocketUnsafe extends IoUringStreamUnsafe {
123 
124         private MsgHdrMemory writeMsgHdrMemory;
125         private MsgHdrMemory readMsgHdrMemory;
126 
127         @Override
128         protected int scheduleWriteSingle(Object msg) {
129             if (msg instanceof FileDescriptor) {
130                 // we can reuse the same memory for any fd
131                 // because we never have more than a single outstanding write.
132                 if (writeMsgHdrMemory == null) {
133                     writeMsgHdrMemory = new MsgHdrMemory();
134                 }
135                 IoRegistration registration = registration();
136                 IoUringIoOps ioUringIoOps = prepSendFdIoOps((FileDescriptor) msg, writeMsgHdrMemory);
137                 writeId = registration.submit(ioUringIoOps);
138                 writeOpCode = Native.IORING_OP_SENDMSG;
139                 if (writeId == 0) {
140                     MsgHdrMemory memory = writeMsgHdrMemory;
141                     writeMsgHdrMemory = null;
142                     memory.release();
143                     return 0;
144                 }
145                 return 1;
146             }
147             return super.scheduleWriteSingle(msg);
148         }
149 
150         @Override
151         boolean writeComplete0(byte op, int res, int flags, short data, int outstanding) {
152             if (op == Native.IORING_OP_SENDMSG) {
153                 writeId = 0;
154                 writeOpCode = 0;
155                 if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
156                     return true;
157                 }
158                 try {
159                     int nativeCallResult = res >= 0 ? res : Errors.ioResult("io_uring sendmsg", res);
160                     if (nativeCallResult >= 0) {
161                         ChannelOutboundBuffer channelOutboundBuffer = unsafe().outboundBuffer();
162                         channelOutboundBuffer.remove();
163                     }
164                 } catch (Throwable throwable) {
165                    handleWriteError(throwable);
166                 }
167                 return true;
168             }
169             return super.writeComplete0(op, res, flags, data, outstanding);
170         }
171 
172         private IoUringIoOps prepSendFdIoOps(FileDescriptor fileDescriptor, MsgHdrMemory msgHdrMemory) {
173             msgHdrMemory.setScmRightsFd(fileDescriptor.intValue());
174             return IoUringIoOps.newSendmsg(
175                     fd().intValue(), (byte) 0, 0, msgHdrMemory.address(), msgHdrMemory.idx());
176         }
177 
178         @Override
179         protected int scheduleRead0(boolean first, boolean socketIsEmpty) {
180             DomainSocketReadMode readMode = config.getReadMode();
181             switch (readMode) {
182                 case FILE_DESCRIPTORS:
183                     return scheduleRecvReadFd();
184                 case BYTES:
185                     return super.scheduleRead0(first, socketIsEmpty);
186                 default:
187                     throw new Error("Unexpected read mode: " + readMode);
188             }
189         }
190 
191         private int scheduleRecvReadFd() {
192             // we can reuse the same memory for any fd
193             // because we only submit one outstanding read
194             if (readMsgHdrMemory == null) {
195                 readMsgHdrMemory = new MsgHdrMemory();
196             }
197             readMsgHdrMemory.prepRecvReadFd();
198             IoRegistration registration = registration();
199             IoUringIoOps ioUringIoOps = IoUringIoOps.newRecvmsg(
200                     fd().intValue(), (byte) 0, 0, readMsgHdrMemory.address(), readMsgHdrMemory.idx());
201             readId = registration.submit(ioUringIoOps);
202             readOpCode = Native.IORING_OP_RECVMSG;
203             if (readId == 0) {
204                 MsgHdrMemory memory = readMsgHdrMemory;
205                 readMsgHdrMemory = null;
206                 memory.release();
207                 return 0;
208             }
209             return 1;
210         }
211 
212         @Override
213         protected void readComplete0(byte op, int res, int flags, short data, int outstanding) {
214             if (op == Native.IORING_OP_RECVMSG) {
215                 readId = 0;
216                 if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
217                     return;
218                 }
219                 final IoUringRecvByteAllocatorHandle allocHandle = recvBufAllocHandle();
220                 final ChannelPipeline pipeline = pipeline();
221                 try {
222                     int nativeCallResult = res >= 0 ? res : Errors.ioResult("io_uring recvmsg", res);
223                     int nativeFd = readMsgHdrMemory.getScmRightsFd();
224                     allocHandle.lastBytesRead(nativeFd);
225                     allocHandle.incMessagesRead(1);
226                     pipeline.fireChannelRead(new FileDescriptor(nativeFd));
227                 } catch (Throwable throwable) {
228                     handleReadException(pipeline, null, throwable, false, allocHandle);
229                 } finally {
230                     allocHandle.readComplete();
231                     pipeline.fireChannelReadComplete();
232                 }
233                 return;
234             }
235             super.readComplete0(op, res, flags, data, outstanding);
236         }
237 
238         @Override
239         public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
240             // Make sure to assign local/remote first before triggering the callback, to prevent potential NPE issues.
241             ChannelPromise channelPromise = newPromise().addListener(new ChannelFutureListener() {
242                 @Override
243                 public void operationComplete(ChannelFuture future) throws Exception {
244                     if (future.isSuccess()) {
245                         local = localAddress != null
246                                 ? (DomainSocketAddress) localAddress
247                                 : socket.localDomainSocketAddress();
248                         remote = (DomainSocketAddress) remoteAddress;
249                         promise.setSuccess();
250                     } else {
251                         promise.setFailure(future.cause());
252                     }
253                 }
254             });
255             super.connect(remoteAddress, localAddress, channelPromise);
256         }
257 
258         @Override
259         public void unregistered() {
260             super.unregistered();
261             if (readMsgHdrMemory != null) {
262                 readMsgHdrMemory.release();
263                 readMsgHdrMemory = null;
264             }
265             if (writeMsgHdrMemory != null) {
266                 writeMsgHdrMemory.release();
267                 writeMsgHdrMemory = null;
268             }
269         }
270     }
271 
272     @Override
273     boolean isPollInFirst() {
274         DomainSocketReadMode readMode = config.getReadMode();
275         switch (readMode) {
276             case BYTES:
277                 return super.isPollInFirst();
278             case FILE_DESCRIPTORS:
279                 return false;
280             default:
281                 throw new Error("Unexpected read mode: " + readMode);
282         }
283     }
284 }