001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2016-2018 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Affero General Public License as published by 007 * the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 013 * for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jgrapes.net; 020 021import java.io.IOException; 022import java.lang.management.ManagementFactory; 023import java.lang.ref.WeakReference; 024import java.net.InetSocketAddress; 025import java.net.SocketAddress; 026import java.net.StandardProtocolFamily; 027import java.net.UnixDomainSocketAddress; 028import java.nio.channels.SelectionKey; 029import java.nio.channels.ServerSocketChannel; 030import java.nio.channels.SocketChannel; 031import java.util.ArrayList; 032import java.util.Collections; 033import java.util.Comparator; 034import java.util.HashSet; 035import java.util.IntSummaryStatistics; 036import java.util.List; 037import java.util.Optional; 038import java.util.Set; 039import java.util.SortedMap; 040import java.util.TreeMap; 041import java.util.stream.Collectors; 042import javax.management.InstanceAlreadyExistsException; 043import javax.management.MBeanRegistrationException; 044import javax.management.MBeanServer; 045import javax.management.MalformedObjectNameException; 046import javax.management.NotCompliantMBeanException; 047import javax.management.ObjectName; 048import org.jgrapes.core.Channel; 049import org.jgrapes.core.Components; 050import org.jgrapes.core.Event; 051import org.jgrapes.core.Manager; 052import org.jgrapes.core.Self; 053import org.jgrapes.core.Subchannel; 054import org.jgrapes.core.annotation.Handler; 055import org.jgrapes.core.events.Error; 056import org.jgrapes.core.events.Start; 057import org.jgrapes.core.events.Stop; 058import org.jgrapes.io.NioHandler; 059import org.jgrapes.io.events.Close; 060import org.jgrapes.io.events.Closed; 061import org.jgrapes.io.events.IOError; 062import org.jgrapes.io.events.Input; 063import org.jgrapes.io.events.NioRegistration; 064import org.jgrapes.io.events.NioRegistration.Registration; 065import org.jgrapes.io.events.Opening; 066import org.jgrapes.io.events.Output; 067import org.jgrapes.io.events.Purge; 068import org.jgrapes.io.util.AvailabilityListener; 069import org.jgrapes.io.util.LinkedIOSubchannel; 070import org.jgrapes.io.util.PermitsPool; 071import org.jgrapes.net.events.Accepted; 072import org.jgrapes.net.events.Ready; 073import org.jgrapes.util.events.ConfigurationUpdate; 074 075/** 076 * Provides a socket server. The server binds to the given address. If the 077 * address is {@code null}, address and port are automatically assigned. 078 * The port may be overwritten by a configuration event 079 * (see {@link #onConfigurationUpdate(ConfigurationUpdate)}). 080 * 081 * For each established connection, the server creates a new 082 * {@link LinkedIOSubchannel}. The servers basic operation is to 083 * fire {@link Input} (and {@link Closed}) events on the 084 * appropriate subchannel in response to data received from the 085 * network and to handle {@link Output} (and {@link Close}) events 086 * on the subchannel and forward the information to the network 087 * connection. 088 * 089 * The server supports limiting the number of concurrent connections 090 * with a {@link PermitsPool}. If such a pool is set as connection 091 * limiter (see {@link #setConnectionLimiter(PermitsPool)}), a 092 * permit is acquired for each new connection attempt. If no more 093 * permits are available, the server sends a {@link Purge} event on 094 * each channel that is purgeable for at least the time span 095 * set with {@link #setMinimalPurgeableTime(long)}. Purgeability 096 * is derived from the end of record flag of {@link Output} events 097 * (see {@link #onOutput(Output, SocketChannelImpl)}. When using this feature, 098 * make sure that connections are either short lived or the application 099 * level components support the {@link Purge} event. Else, it may become 100 * impossible to establish new connections. 101 */ 102@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.ExcessivePublicCount", 103 "PMD.NcssCount", "PMD.EmptyCatchBlock", "PMD.AvoidDuplicateLiterals", 104 "PMD.ExcessiveClassLength", "PMD.CouplingBetweenObjects" }) 105public class SocketServer extends SocketConnectionManager 106 implements NioHandler { 107 108 private SocketAddress serverAddress; 109 private ServerSocketChannel serverSocketChannel; 110 private boolean closing; 111 private int backlog; 112 private PermitsPool connLimiter; 113 private Registration registration; 114 @SuppressWarnings("PMD.SingularField") 115 private Purger purger; 116 private long minimumPurgeableTime; 117 118 /** 119 * The purger thread. 120 */ 121 private class Purger extends Thread implements AvailabilityListener { 122 123 private boolean permitsAvailable = true; 124 125 /** 126 * Instantiates a new purger. 127 */ 128 public Purger() { 129 setName(Components.simpleObjectName(this)); 130 setDaemon(true); 131 } 132 133 @Override 134 public void availabilityChanged(PermitsPool pool, boolean available) { 135 if (registration == null) { 136 return; 137 } 138 synchronized (this) { 139 permitsAvailable = available; 140 registration.updateInterested( 141 permitsAvailable ? SelectionKey.OP_ACCEPT : 0); 142 if (!permitsAvailable) { 143 this.notifyAll(); 144 } 145 } 146 } 147 148 @Override 149 @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", 150 "PMD.DataflowAnomalyAnalysis", "PMD.CognitiveComplexity" }) 151 public void run() { 152 if (connLimiter == null) { 153 return; 154 } 155 try { 156 connLimiter.addListener(this); 157 while (serverSocketChannel.isOpen()) { 158 synchronized (this) { 159 while (permitsAvailable) { 160 wait(); 161 } 162 } 163 // Copy to avoid ConcurrentModificationException 164 List<SocketChannelImpl> candidates; 165 synchronized (channels) { 166 candidates = new ArrayList<>(channels); 167 } 168 long purgeableSince 169 = System.currentTimeMillis() - minimumPurgeableTime; 170 candidates = candidates.stream() 171 .filter(channel -> channel.isPurgeable() 172 && channel.purgeableSince() < purgeableSince) 173 .sorted(new Comparator<>() { 174 @Override 175 @SuppressWarnings("PMD.ShortVariable") 176 public int compare(SocketChannelImpl c1, 177 SocketChannelImpl c2) { 178 if (c1.purgeableSince() < c2 179 .purgeableSince()) { 180 return 1; 181 } 182 if (c1.purgeableSince() > c2 183 .purgeableSince()) { 184 return -1; 185 } 186 return 0; 187 } 188 }) 189 .collect(Collectors.toList()); 190 for (SocketChannelImpl channel : candidates) { 191 // Sorting may have taken time... 192 if (!channel.isPurgeable()) { 193 continue; 194 } 195 channel.downPipeline().fire(new Purge(), channel); 196 // Continue only as long as necessary 197 if (permitsAvailable) { 198 break; 199 } 200 } 201 sleep(1000); 202 } 203 } catch (InterruptedException e) { 204 // Fall through 205 } finally { 206 connLimiter.removeListener(this); 207 } 208 } 209 210 } 211 212 /** 213 * Creates a new server, using itself as component channel. 214 */ 215 public SocketServer() { 216 this(Channel.SELF); 217 } 218 219 /** 220 * Creates a new server using the given channel. 221 * 222 * @param componentChannel the component's channel 223 */ 224 public SocketServer(Channel componentChannel) { 225 super(componentChannel); 226 } 227 228 /** 229 * Sets the address to bind to. If none is set, the address and port 230 * are assigned automatically. 231 * 232 * @param serverAddress the address to bind to 233 * @return the socket server for easy chaining 234 */ 235 public SocketServer setServerAddress(SocketAddress serverAddress) { 236 this.serverAddress = serverAddress; 237 return this; 238 } 239 240 @Override 241 public SocketServer setBufferSize(int size) { 242 super.setBufferSize(size); 243 return this; 244 } 245 246 /** 247 * The component can be configured with events that include 248 * a path (see @link {@link ConfigurationUpdate#paths()}) 249 * that matches this components path (see {@link Manager#componentPath()}). 250 * 251 * The following properties are recognized: 252 * 253 * `hostname` 254 * : If given, is used as first parameter for 255 * {@link InetSocketAddress#InetSocketAddress(String, int)}. 256 * 257 * `port` 258 * : If given, is used as parameter for 259 * {@link InetSocketAddress#InetSocketAddress(String, int)} 260 * or {@link InetSocketAddress#InetSocketAddress(int)}, 261 * depending on whether a host name is specified. Defaults to "0". 262 * 263 * `backlog` 264 * : See {@link #setBacklog(int)}. 265 * 266 * `bufferSize` 267 * : See {@link #setBufferSize(int)}. 268 * 269 * `maxConnections` 270 * : Calls {@link #setConnectionLimiter} with a 271 * {@link PermitsPool} of the specified size. 272 * 273 * `minimalPurgeableTime` 274 * : See {@link #setMinimalPurgeableTime(long)}. 275 * 276 * @param event the event 277 */ 278 @Handler 279 @SuppressWarnings("PMD.ConfusingTernary") 280 public void onConfigurationUpdate(ConfigurationUpdate event) { 281 event.values(componentPath()).ifPresent(values -> { 282 String hostname = values.get("hostname"); 283 if (hostname != null) { 284 setServerAddress(new InetSocketAddress(hostname, 285 Integer.parseInt(values.getOrDefault("port", "0")))); 286 } else if (values.containsKey("port")) { 287 setServerAddress(new InetSocketAddress( 288 Integer.parseInt(values.get("port")))); 289 } 290 Optional.ofNullable(values.get("backlog")).ifPresent( 291 value -> setBacklog(Integer.parseInt(value))); 292 Optional.ofNullable(values.get("bufferSize")).ifPresent( 293 value -> setBufferSize(Integer.parseInt(value))); 294 Optional.ofNullable(values.get("maxConnections")) 295 .map(Integer::parseInt).map(PermitsPool::new) 296 .ifPresent(this::setConnectionLimiter); 297 Optional.ofNullable(values.get("minimalPurgeableTime")) 298 .map(Long::parseLong).ifPresent(this::setMinimalPurgeableTime); 299 }); 300 } 301 302 /** 303 * Returns the server address. Before starting, the address is the 304 * address set with {@link #setServerAddress(InetSocketAddress)}. After 305 * starting the address is obtained from the created socket. 306 * 307 * @return the serverAddress 308 */ 309 public SocketAddress serverAddress() { 310 try { 311 return serverSocketChannel == null ? serverAddress 312 : serverSocketChannel.getLocalAddress(); 313 } catch (IOException e) { 314 return serverAddress; 315 } 316 } 317 318 /** 319 * Sets the backlog size. 320 * 321 * @param backlog the backlog to set 322 * @return the socket server for easy chaining 323 */ 324 public SocketServer setBacklog(int backlog) { 325 this.backlog = backlog; 326 return this; 327 } 328 329 /** 330 * Return the configured backlog size. 331 * 332 * @return the backlog 333 */ 334 public int backlog() { 335 return backlog; 336 } 337 338 /** 339 * Sets a permit "pool". A new connection is created only if a permit 340 * can be obtained from the pool. 341 * 342 * A connection limiter must be set before starting the component. 343 * 344 * @param connectionLimiter the connection pool to set 345 * @return the socket server for easy chaining 346 */ 347 public SocketServer setConnectionLimiter(PermitsPool connectionLimiter) { 348 this.connLimiter = connectionLimiter; 349 return this; 350 } 351 352 /** 353 * Returns the connection limiter. 354 * 355 * @return the connection Limiter 356 */ 357 public PermitsPool getConnectionLimiter() { 358 return connLimiter; 359 } 360 361 /** 362 * Sets a minimal time that a connection must be purgeable (idle) 363 * before it may be purged. 364 * 365 * @param millis the millis 366 * @return the socket server 367 */ 368 public SocketServer setMinimalPurgeableTime(long millis) { 369 this.minimumPurgeableTime = millis; 370 return this; 371 } 372 373 /** 374 * Gets the minimal purgeable time. 375 * 376 * @return the minimal purgeable time 377 */ 378 public long getMinimalPurgeableTime() { 379 return minimumPurgeableTime; 380 } 381 382 /** 383 * Starts the server. 384 * 385 * @param event the start event 386 * @throws IOException if an I/O exception occurred 387 */ 388 @Handler 389 public void onStart(Start event) throws IOException { 390 closing = false; 391 if (serverAddress instanceof UnixDomainSocketAddress) { 392 serverSocketChannel 393 = ServerSocketChannel.open(StandardProtocolFamily.UNIX); 394 } else { 395 serverSocketChannel = ServerSocketChannel.open(); 396 } 397 serverSocketChannel.bind(serverAddress, backlog); 398 MBeanView.addServer(this); 399 fire(new NioRegistration(this, serverSocketChannel, 400 SelectionKey.OP_ACCEPT, this), Channel.BROADCAST); 401 } 402 403 /** 404 * Handles the successful channel registration. 405 * 406 * @param event the event 407 * @throws InterruptedException the interrupted exception 408 * @throws IOException Signals that an I/O exception has occurred. 409 */ 410 @Handler(channels = Self.class) 411 public void onRegistered(NioRegistration.Completed event) 412 throws InterruptedException, IOException { 413 NioHandler handler = event.event().handler(); 414 if (handler == this) { 415 if (event.event().get() == null) { 416 fire(new Error(event, 417 "Registration failed, no NioDispatcher?")); 418 return; 419 } 420 registration = event.event().get(); 421 purger = new Purger(); 422 purger.start(); 423 fire(new Ready(serverSocketChannel.getLocalAddress())); 424 return; 425 } 426 if (handler instanceof SocketChannelImpl channel 427 && channels.contains(channel)) { 428 var accepted = new Accepted(channel.nioChannel().getLocalAddress(), 429 channel.nioChannel().getRemoteAddress(), false, 430 Collections.emptyList()); 431 var registration = event.event().get(); 432 // (1) Opening, (2) Accepted, (3) process input 433 channel.downPipeline().fire(Event.onCompletion(new Opening<Void>(), 434 e -> { 435 channel.downPipeline().fire(accepted, channel); 436 channel.registrationComplete(registration); 437 }), channel); 438 } 439 } 440 441 /* 442 * (non-Javadoc) 443 * 444 * @see org.jgrapes.io.NioSelectable#handleOps(int) 445 */ 446 @Override 447 public void handleOps(int ops) { 448 if ((ops & SelectionKey.OP_ACCEPT) == 0 || closing) { 449 return; 450 } 451 synchronized (channels) { 452 if (connLimiter != null && !connLimiter.tryAcquire()) { 453 return; 454 } 455 try { 456 @SuppressWarnings("PMD.CloseResource") 457 SocketChannel socketChannel = serverSocketChannel.accept(); 458 if (socketChannel == null) { 459 // "False alarm" 460 if (connLimiter != null) { 461 connLimiter.release(); 462 } 463 return; 464 } 465 new SocketChannelImpl(null, socketChannel); 466 } catch (IOException e) { 467 fire(new IOError(null, e)); 468 } 469 } 470 } 471 472 @Override 473 protected boolean removeChannel(SocketChannelImpl channel) { 474 synchronized (channels) { 475 if (!channels.remove(channel)) { 476 // Closed already 477 return false; 478 } 479 // In case the server is shutting down 480 channels.notifyAll(); 481 } 482 if (connLimiter != null) { 483 connLimiter.release(); 484 } 485 return true; 486 } 487 488 /** 489 * Shuts down the server or one of the connections to the server. 490 * 491 * @param event the event 492 * @throws IOException if an I/O exception occurred 493 * @throws InterruptedException if the execution was interrupted 494 */ 495 @Handler 496 @SuppressWarnings("PMD.DataflowAnomalyAnalysis") 497 public void onClose(Close event) throws IOException, InterruptedException { 498 boolean closeServer = false; 499 for (Channel channel : event.channels()) { 500 if (channels.contains(channel)) { 501 ((SocketChannelImpl) channel).close(); 502 continue; 503 } 504 if (channel instanceof Subchannel) { 505 // Some subchannel that we're not interested in. 506 continue; 507 } 508 // Close event on "main" channel 509 closeServer = true; 510 } 511 if (!closeServer) { 512 // Only connection(s) were to be closed. 513 return; 514 } 515 if (!serverSocketChannel.isOpen()) { 516 // Closed already 517 fire(new Closed<Void>()); 518 return; 519 } 520 synchronized (channels) { 521 closing = true; 522 // Copy to avoid concurrent modification exception 523 Set<SocketChannelImpl> conns = new HashSet<>(channels); 524 for (SocketChannelImpl conn : conns) { 525 conn.close(); 526 } 527 while (!channels.isEmpty()) { 528 channels.wait(); 529 } 530 } 531 serverSocketChannel.close(); 532 purger.interrupt(); 533 closing = false; 534 fire(new Closed<Void>()); 535 } 536 537 /** 538 * Shuts down the server by firing a {@link Close} using the 539 * server as channel. Note that this automatically results 540 * in closing all open connections by the runtime system 541 * and thus in {@link Closed} events on all subchannels. 542 * 543 * @param event the event 544 * @throws InterruptedException 545 */ 546 @Handler(priority = -1000) 547 public void onStop(Stop event) throws InterruptedException { 548 if (closing || !serverSocketChannel.isOpen()) { 549 return; 550 } 551 newEventPipeline().fire(new Close(), this).get(); 552 } 553 554 /** 555 * The Interface of the SocketServer MXBean. 556 */ 557 public interface SocketServerMXBean { 558 559 /** 560 * The Class ChannelInfo. 561 */ 562 class ChannelInfo { 563 564 private final SocketChannelImpl channel; 565 566 /** 567 * Instantiates a new channel info. 568 * 569 * @param channel the channel 570 */ 571 public ChannelInfo(SocketChannelImpl channel) { 572 this.channel = channel; 573 } 574 575 /** 576 * Checks if is purgeable. 577 * 578 * @return true, if is purgeable 579 */ 580 public boolean isPurgeable() { 581 return channel.isPurgeable(); 582 } 583 584 /** 585 * Gets the downstream pool. 586 * 587 * @return the downstream pool 588 */ 589 public String getDownstreamPool() { 590 return channel.readBuffers().name(); 591 } 592 593 /** 594 * Gets the upstream pool. 595 * 596 * @return the upstream pool 597 */ 598 public String getUpstreamPool() { 599 return channel.byteBufferPool().name(); 600 } 601 } 602 603 /** 604 * Gets the component path. 605 * 606 * @return the component path 607 */ 608 String getComponentPath(); 609 610 /** 611 * Gets the channel count. 612 * 613 * @return the channel count 614 */ 615 int getChannelCount(); 616 617 /** 618 * Gets the channels. 619 * 620 * @return the channels 621 */ 622 SortedMap<String, ChannelInfo> getChannels(); 623 624 } 625 626 /** 627 * The Class SocketServerInfo. 628 */ 629 public static class SocketServerInfo implements SocketServerMXBean { 630 631 private static MBeanServer mbs 632 = ManagementFactory.getPlatformMBeanServer(); 633 634 private ObjectName mbeanName; 635 private final WeakReference<SocketServer> serverRef; 636 637 /** 638 * Instantiates a new socket server info. 639 * 640 * @param server the server 641 */ 642 @SuppressWarnings({ "PMD.EmptyCatchBlock", 643 "PMD.AvoidCatchingGenericException", 644 "PMD.ConstructorCallsOverridableMethod" }) 645 public SocketServerInfo(SocketServer server) { 646 serverRef = new WeakReference<>(server); 647 try { 648 String endPoint = ""; 649 if (server.serverAddress instanceof InetSocketAddress addr) { 650 endPoint = " (" + addr.getHostName() + ":" + addr.getPort() 651 + ")"; 652 } else if (server.serverAddress instanceof UnixDomainSocketAddress addr) { 653 endPoint = " (" + addr.getPath() + ")"; 654 } 655 mbeanName = new ObjectName("org.jgrapes.io:type=" 656 + SocketServer.class.getSimpleName() + ",name=" 657 + ObjectName 658 .quote(Components.objectName(server) + endPoint)); 659 } catch (MalformedObjectNameException e) { 660 // Should not happen 661 } 662 try { 663 mbs.unregisterMBean(mbeanName); 664 } catch (Exception e) { 665 // Just in case, should not work 666 } 667 try { 668 mbs.registerMBean(this, mbeanName); 669 } catch (InstanceAlreadyExistsException | MBeanRegistrationException 670 | NotCompliantMBeanException e) { 671 // Have to live with that 672 } 673 } 674 675 /** 676 * Server. 677 * 678 * @return the optional 679 */ 680 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 681 "PMD.EmptyCatchBlock" }) 682 public Optional<SocketServer> server() { 683 SocketServer server = serverRef.get(); 684 if (server == null) { 685 try { 686 mbs.unregisterMBean(mbeanName); 687 } catch (Exception e) { 688 // Should work. 689 } 690 } 691 return Optional.ofNullable(server); 692 } 693 694 @Override 695 public String getComponentPath() { 696 return server().map(mgr -> mgr.componentPath()).orElse("<removed>"); 697 } 698 699 @Override 700 public int getChannelCount() { 701 return server().map(server -> server.channels.size()).orElse(0); 702 } 703 704 @Override 705 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 706 public SortedMap<String, ChannelInfo> getChannels() { 707 return server().map(server -> { 708 SortedMap<String, ChannelInfo> result = new TreeMap<>(); 709 for (SocketChannelImpl channel : server.channels) { 710 result.put(channel.nioChannel().socket() 711 .getRemoteSocketAddress().toString(), 712 new ChannelInfo(channel)); 713 } 714 return result; 715 }).orElse(Collections.emptySortedMap()); 716 } 717 } 718 719 /** 720 * An MBean interface for getting information about the socket servers 721 * and established connections. 722 */ 723 public interface SocketServerSummaryMXBean { 724 725 /** 726 * Gets the connections per server statistics. 727 * 728 * @return the connections per server statistics 729 */ 730 IntSummaryStatistics getConnectionsPerServerStatistics(); 731 732 /** 733 * Gets the servers. 734 * 735 * @return the servers 736 */ 737 Set<SocketServerMXBean> getServers(); 738 } 739 740 /** 741 * The MBeanView. 742 */ 743 private static final class MBeanView implements SocketServerSummaryMXBean { 744 private static Set<SocketServerInfo> serverInfos = new HashSet<>(); 745 746 /** 747 * Adds the server to the reported servers. 748 * 749 * @param server the server 750 */ 751 public static void addServer(SocketServer server) { 752 synchronized (serverInfos) { 753 serverInfos.add(new SocketServerInfo(server)); 754 } 755 } 756 757 /** 758 * Returns the infos. 759 * 760 * @return the sets the 761 */ 762 private Set<SocketServerInfo> infos() { 763 Set<SocketServerInfo> expired = new HashSet<>(); 764 synchronized (serverInfos) { 765 for (SocketServerInfo serverInfo : serverInfos) { 766 if (!serverInfo.server().isPresent()) { 767 expired.add(serverInfo); 768 } 769 } 770 serverInfos.removeAll(expired); 771 } 772 return serverInfos; 773 } 774 775 @SuppressWarnings("unchecked") 776 @Override 777 public Set<SocketServerMXBean> getServers() { 778 return (Set<SocketServerMXBean>) (Object) infos(); 779 } 780 781 @Override 782 public IntSummaryStatistics getConnectionsPerServerStatistics() { 783 return infos().stream().map(info -> info.server().get()) 784 .filter(ref -> ref != null).collect( 785 Collectors.summarizingInt(srv -> srv.channels.size())); 786 } 787 } 788 789 static { 790 try { 791 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 792 ObjectName mxbeanName = new ObjectName("org.jgrapes.io:type=" 793 + SocketServer.class.getSimpleName() + "s"); 794 mbs.registerMBean(new MBeanView(), mxbeanName); 795 } catch (MalformedObjectNameException | InstanceAlreadyExistsException 796 | MBeanRegistrationException | NotCompliantMBeanException e) { 797 // Does not happen 798 } 799 } 800 801}