nginx中锁的设计以及惊群的处理

nginx中使用的锁是自己来实现的,这里锁的实现分为两种情况,一种是支持原子操作的情况,也就是由NGX_HAVE_ATOMIC_OPS这个宏来进行控制的,一种是不支持原子操作,这是是使用文件锁来实现。

首先我们要知道在用户空间进程间锁实现的原理,起始原理很简单,就是能弄一个让所有进程共享的东西,比如mmap的内存,比如文件,然后通过这个东西来控制进程的互斥。

说起来锁很简单,就是共享一个变量,然后通过设置这个变量来控制进程的行为。

我们先来看核心的数据结构,也就是说用来控制进程的互斥的东西。

这个数据结构可以看到和我上面讲得一样,那就是通过宏来分成两种。

1 如果支持原子操作,则我们可以直接使用mmap,然后lock就保存mmap的内存区域的地址

2 如果不支持原子操作,则我们使用文件锁来实现,这里fd表示进程间共享的文件句柄,name表示文件名。

Java代码 收藏代码
  1. typedef struct {
  2. #if (NGX_HAVE_ATOMIC_OPS)
  3. ngx_atomic_t *lock;
  4. #else
  5. ngx_fd_t fd;
  6. u_char *name;
  7. #endif
  8. } ngx_shmtx_t;


接着来看代码,先来看支持原子操作的情况下的实现方式。这里要注意下,下面的函数基本都会有两个实现,一个是支持原子操作,一个是不支持的,我这里全部都是分开来分析的。

先来看初始化,初始化代码在ngx_event_module_init中。

下面这段代码是设置将要设置的共享区域的大小,这里cl的大小最好是要大于或者等于cache line。
通过代码可以看到这里将会有3个区域被所有进程共享,其中我们的锁将会用到的是第一个。
Java代码 收藏代码
  1. size_t size, cl;
  2. cl = 128;
  3. //可以看到三个区域。
  4. size = cl /* ngx_accept_mutex */
  5. + cl /* ngx_connection_counter */
  6. + cl; /* ngx_temp_number */



下面这段代码是初始化对应的共享内存区域。然后保存对应的互斥体指针。
Java代码 收藏代码
  1. //这个是一个全局变量,保存的是共享区域的指针。
  2. ngx_atomic_t *ngx_accept_mutex_ptr;
  3. //这个就是我们上面介绍的互斥体。
  4. ngx_shmtx_t ngx_accept_mutex;
  5. ngx_shm_t shm;
  6. //开始初始化
  7. shm.size = size;
  8. shm.name.len = sizeof("nginx_shared_zone");
  9. shm.name.data = (u_char *) "nginx_shared_zone";
  10. shm.log = cycle->log;
  11. //分配对应的内存,使用mmap或者shm之类的。
  12. if (ngx_shm_alloc(&shm) != NGX_OK) {
  13. return NGX_ERROR;
  14. }
  15. shared = shm.addr;
  16. ngx_accept_mutex_ptr = (ngx_atomic_t *) shared;
  17. //初始化互斥体。
  18. if (ngx_shmtx_create(&ngx_accept_mutex, shared, cycle->lock_file.data)
  19. != NGX_OK)
  20. {
  21. return NGX_ERROR;
  22. }


下面我们来看ngx_shmtx_create的实现。
可以看到如果支持原子操作的话,非常简单,就是将共享内存的地址付给loc这个域。
Java代码 收藏代码
  1. ngx_int_t
  2. ngx_shmtx_create(ngx_shmtx_t *mtx, void *addr, u_char *name)
  3. {
  4. mtx->lock = addr;
  5. return NGX_OK;
  6. }


然后来看nginx中如何来获得锁,以及释放锁。

我们先来看获得锁。

这里nginx分为两个函数,一个是trylock,它是非阻塞的,也就是说它会尝试的获得锁,如果没有获得的话,它会直接返回错误。

而第二个是lock,它也会尝试获得锁,而当没有获得他不会立即返回,而是开始进入循环然后不停的去获得锁,知道获得。不过nginx这里还有用到一个技巧,就是每次都会让当前的进程放到cpu的运行队列的最后一位,也就是自动放弃cpu。

先来看trylock

这个很简单,首先判断lock是否为0,为0的话表示可以获得锁,因此我们就调用ngx_atomic_cmp_set去获得锁,如果获得成功就会返回1,负责为0.

Java代码 收藏代码
  1. static ngx_inline ngx_uint_t
  2. ngx_shmtx_trylock(ngx_shmtx_t *mtx)
  3. {
  4. return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));
  5. }


接下来详细描述下ngx_atomic_cmp_set,这里这个操作是一个原子操作,这是因为由于我们要进行比较+赋值两个操作,如果不是原子操作的话,有可能在比较之后被其他进程所抢占,此时再赋值的话就会有问题了,因此这里就必须是一个原子操作。

我们来看这个函数的实现,如果系统库不支持这个指令的话,nginx自己还用汇编实现了一个,其实实现也很简单,比如x86的话有一个cmpxchgl的指令,就是做这个的。

先来看如果系统库支持的情况,此时直接调用OSAtomicCompareAndSwap32Barrier。

Java代码 收藏代码
  1. #define ngx_atomic_cmp_set(lock, old, new) \
  2. OSAtomicCompareAndSwap32Barrier(old, new, (int32_t *) lock)


来看函数的原型:
Java代码 收藏代码
  1. OSAtomicCompareAndSwap32Barrier(old, new, addr)


然后这个函数翻译成伪码的话就是这个:

Java代码 收藏代码
  1. f (*addr == oldvalue) {
  2. *addr = newvalue;
  3. return 1;
  4. } else {
  5. return 0;
  6. }


这个代码就不解释了,很浅显易懂。

因此上面的trylock的代码:
Java代码 收藏代码
  1. ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)
的意思就是如果lock的值是0的话,就把lock的值修改为当前的进程id,否则返回失败。

然后来看这个的汇编实现,这里nginx实现了多个平台的比如x86,sparc,ppc.
我们来看x86的:

Java代码 收藏代码
  1. static ngx_inline ngx_atomic_uint_t
  2. ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,
  3. ngx_atomic_uint_t set)
  4. {
  5. u_char res;
  6. __asm__ volatile (
  7. NGX_SMP_LOCK
  8. " cmpxchgl %3, %1; "
  9. " sete %0; "
  10. : "=a" (res) : "m" (*lock), "a" (old), "r" (set) : "cc", "memory");
  11. return res;
  12. }


具体的这些指令和锁可以去看intel的相关手册。

接下来来看lock的实现,lock最终会调用ngx_spinlock,因此下面我要主要来分析这个函数。
Java代码 收藏代码
  1. #define ngx_shmtx_lock(mtx) ngx_spinlock((mtx)->lock, ngx_pid, 1024)


我们来看spinklock,必须支持原子指令,才会有这个函数,这里nginx采用宏来控制的.

这里和trylock的处理差不多,都是利用原子指令来实现的,只不过这里如果无法获得锁,则会继续等待。

我们来看代码的实现:

Java代码 收藏代码
  1. void
  2. ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)
  3. {
  4. #if (NGX_HAVE_ATOMIC_OPS)
  5. ngx_uint_t i, n;
  6. for ( ;; ) {
  7. //如果lock为0,则说明没有进程持有锁,因此设置lock为value(为当前进程id),然后返回。
  8. if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
  9. return;
  10. }
  11. //如果cpu个数大于1(也就是多核),则进入spin-wait loop阶段。
  12. if (ngx_ncpu > 1) {
  13. //开始进入循环。
  14. for (n = 1; n < spin; n <<= 1) {
  15. //下面这段就是纯粹的spin-loop wait。
  16. for (i = 0; i < n; i++) {
  17. //这个函数其实就是执行"PAUSE"指令,接下来会解释这个指令。
  18. ngx_cpu_pause();
  19. }
  20. //然后重新获取锁,如果获得则直接返回。
  21. if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
  22. return;
  23. }
  24. }
  25. }
  26. //这个函数调用的是sched_yield,它会强迫当前运行的进程放弃占有处理器。
  27. ngx_sched_yield();
  28. }
  29. #else
  30. #if (NGX_THREADS)
  31. #error ngx_spinlock() or ngx_atomic_cmp_set() are not defined !
  32. #endif
  33. #endif
  34. }


通过上面的代码可以看到spin lock实现的很简单,就是一个如果无法获得锁,就进入忙等的过程,不过这里nginx还多加了一个处理,就是如果忙等太长,就放弃cpu,直到下次任务再次占有cpu。

接下来来看下PAUSE指令,这条指令主要的功能就是告诉cpu,我现在是一个spin-wait loop,然后cpu就不会因为害怕循环退出时,内存的乱序而需要处理,所引起的效率损失问题。

下面就是intel手册的解释:

引用

Improves the performance of spin-wait loops. When executing a “spin-wait loop,” a
Pentium 4 or Intel Xeon processor suffers a severe performance penalty when exiting
the loop because it detects a possible memory order violation. The PAUSE instruction
provides a hint to the processor that the code sequence is a spin-wait loop. The
processor uses this hint to avoid the memory order violation in most situations,
which greatly improves processor performance. For this reason, it is recommended
that a PAUSE instruction be placed in all spin-wait loops.


内核的spin lock也有用到这条指令的。

接下来就是unlokck。unlock比较简单,就是和当前进程id比较,如果相等,就把lock改为0,说明放弃这个锁。

Java代码 收藏代码
  1. #define ngx_shmtx_unlock(mtx) (void) ngx_atomic_cmp_set((mtx)->lock, ngx_pid, 0)


然后就是不支持原子操作的情况,此时使用文件锁来实现的,这里就不介绍这种实现了,基本原来和上面的差不多,想要了解的,可以去看nginx的相关代码。

接下来我们来看nginx如何利用lock来控制子进程的负载均衡以及惊群。

先来大概解释下这两个概念。

负载均衡是为了解决有可能一个进程处理了多个连接,因此就需要让多个进程更平均的处理连接。

惊群也就是当我们多个进程阻塞在epoll这类调用的时候,当有数据可读的时候,多个进程会被同时唤醒,此时如果去accept的话,只能有一个进程accept到句柄。

在看代码之前,我们先来看ngx_use_accept_mutex这个变量,如果有这个变量,说明nginx有必要使用accept互斥体,这个变量的初始化在ngx_event_process_init中。

这里还有两个变量,一个是ngx_accept_mutex_held,一个是ngx_accept_mutex_delay,其中前一个表示当前是否已经持有锁,后一个表示,当获得锁失败后,再次去请求锁的间隔时间,这个时间可以看到可以在配置文件中设置的。

Java代码 收藏代码
  1. //如果使用了master worker,并且worker个数大于1,并且配置文件里面有设置使用accept_mutex.的话,设置ngx_use_accept_mutex
  2. if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {
  3. ngx_use_accept_mutex = 1;
  4. //下面这两个变量后面会解释。
  5. ngx_accept_mutex_held = 0;
  6. ngx_accept_mutex_delay = ecf->accept_mutex_delay;
  7. } else {
  8. ngx_use_accept_mutex = 0;
  9. }



这里还有一个变量是ngx_accept_disabled,这个变量是一个阈值,如果大于0,说明当前的进程处理的连接过多。
下面就是这个值的初始化,可以看到初始值是全部连接的7/8(注意是负值0.

Java代码 收藏代码
  1. ngx_accept_disabled = ngx_cycle->connection_n / 8
  2. - ngx_cycle->free_connection_n;


然后来看ngx_process_events_and_timers中的处理。

Java代码 收藏代码
  1. //如果有使用mutex,则才会进行处理。
  2. if (ngx_use_accept_mutex) {
  3. //如果大于0,则跳过下面的锁的处理,并减一。
  4. if (ngx_accept_disabled > 0) {
  5. ngx_accept_disabled--;
  6. } else {
  7. //试着获得锁,如果出错则返回。
  8. if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
  9. return;
  10. }
  11. //如果ngx_accept_mutex_held为1,则说明已经获得锁,此时设置flag,这个flag后面会解释。
  12. if (ngx_accept_mutex_held) {
  13. flags |= NGX_POST_EVENTS;
  14. } else {
  15. //否则,设置timer,也就是定时器。接下来会解释这段。
  16. if (timer == NGX_TIMER_INFINITE
  17. || timer > ngx_accept_mutex_delay)
  18. {
  19. timer = ngx_accept_mutex_delay;
  20. }
  21. }
  22. }
  23. }


然后先来看NGX_POST_EVENTS标记,设置了这个标记就说明当socket有数据被唤醒时,我们并不会马上accept或者说读取,而是将这个事件保存起来,然后当我们释放锁之后,才会进行accept或者读取这个句柄。

Java代码 收藏代码
  1. //如果ngx_posted_accept_events不为NULL,则说明有accept event需要nginx处理。
  2. if (ngx_posted_accept_events) {
  3. ngx_event_process_posted(cycle, &ngx_posted_accept_events);
  4. }


而如果没有设置NGX_POST_EVENTS标记的话,nginx会立即accept或者读取句柄。

然后是定时器,这里如果nginx没有获得锁,并不会马上再去获得锁,而是设置定时器,然后在epoll休眠(如果没有其他的东西唤醒).此时如果有连接到达,当前休眠进程会被提前唤醒,然后立即accept。否则,休眠 ngx_accept_mutex_delay时间,然后继续try lock.

最后是核心的一个函数,那就是ngx_trylock_accept_mutex。这个函数用来尝试获得accept mutex.

Java代码 收藏代码
  1. ngx_int_t
  2. ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
  3. {
  4. //尝试获得锁
  5. if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
  6. //如果本来已经获得锁,则直接返回Ok
  7. if (ngx_accept_mutex_held
  8. && ngx_accept_events == 0
  9. && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
  10. {
  11. return NGX_OK;
  12. }
  13. //到达这里,说明重新获得锁成功,因此需要打开被关闭的listening句柄。
  14. if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
  15. ngx_shmtx_unlock(&ngx_accept_mutex);
  16. return NGX_ERROR;
  17. }
  18. ngx_accept_events = 0;
  19. //设置获得锁的标记。
  20. ngx_accept_mutex_held = 1;
  21. return NGX_OK;
  22. }
  23. //如果我们前面已经获得了锁,然后这次获得锁失败,则说明当前的listen句柄已经被其他的进程锁监听,因此此时需要从epoll中移出调已经注册的listen句柄。这样就很好的控制了子进程的负载均衡
  24. if (ngx_accept_mutex_held) {
  25. if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
  26. return NGX_ERROR;
  27. }
  28. //设置锁的持有为0.
  29. ngx_accept_mutex_held = 0;
  30. }
  31. return NGX_OK;
  32. }


这里可以看到大部分情况下,每次只会有一个进程在监听listen句柄,而只有当ngx_accept_disabled大于0的情况下,才会出现一定程度的惊群。

而nginx中,由于锁的控制(以及获得锁的定时器),每个进程都能相对公平的accept句柄,也就是比较好的解决了子进程负载均衡。

惊群问题的思考

什么是惊群

        举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉,等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:
1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。
2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。

        最常见的例子就是对于socket描述符的accept操作,当多个用户进程/线程监听在同一个端口上时,由于实际只可能accept一次,因此就会产生惊群现象,当然前面已经说过了,这个问题是一个古老的问题,新的操作系统内核已经解决了这一问题。

linux内核解决惊群问题的方法

        对于一些已知的惊群问题,内核开发者增加了一个"互斥等待"选项。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:
        1)当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.
        2)当 wake_up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。
        也就是说,对于互斥等待的行为,比如如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此时间的队列的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。

根据以上背景信息,我们来比较一下常见的Server端设计方案。
方案1:listen后,启动多个线程(进程),对此socket进行监听(仅阻塞accept方式不惊群)。
方案2:主线程负责监听,通过线程池方式处理连接。(通常的方法)
方案3:主线程负责监听,客户端连接上来后由主线程分配实际的端口,客户端根据此端口重新连接,然后处理数据。

先考虑客户端单连接的情况
方案1:每当有新的连接到来时,系统内核会从队列中以FIFO的方式选择一个监听线程来服务此连接,因此可以充分发挥系统的系能并且多线程负载均衡。对于单连接的场景,这种方案无疑是非常优越的。遗憾的是,对于select、epoll,内核目前无法解决惊群问题。(nginx对于惊群问题的解决方法)
方案2:由于只有一个线程在监听,其瞬时的并发处理连接请求的能力必然不如多线程。同时,需要对线程池做调度管理,必然涉及资源共享访问,相对于方案一来说管理成本要增加不少,代码复杂度提高,性能也有所下降。
方案3:与方案2有不少类似的地方,其优势是不需要做线程调度。缺点是增加了主线程的负担,除了接收连接外还需要发送数据,而且需要两次连接,孰优孰劣,有待测试。

再考虑客户端多连接的情况:
对于数据传输类的应用,为了充分利用带宽,往往会开启多个连接来传输数据,连接之间的数据有相互依赖性,因此Server端要想很好的维护这种依赖性,把同一个客户端的所有连接放在一个线程中处理是非常有必要的。
A、同一客户端在一个线程中处理
方案1:如果没有更底层的解决方案的话,Server则需要维护一个全局列表,来记录当前连接请求该由哪个线程处理。多线程需要同时竞争一个全局资源,似乎有些不妙。
方案2:主线程负责监听并分发,因此与单连接相比没有带来额外的性能开销。仅仅会造成主线程忙于更多的连接请求。
方案3:较单线程来说,主线程工作量没有任何增加,由于多连接而造成的额外开销由实际工作线程分担,因此对于这种场景,方案3似乎是最佳选择。

B、同一客户端在不同线程中处理
方案1:同样需要竞争资源。
方案2:没理由。
方案3:不可能。

另外:
(《UNIX网络编程》第三版是在第30章)
读《UNIX网络编程》第二版的第一卷时,发现作者在第27章"客户-服务器程序其它设计方法"中的27.6节"TCP预先派生子进程服务器程序,accept无上锁保护"中提到了一种由子进程去竞争客户端连接的设计方法,用伪码描述如下:

服务器主进程:

listen_fd = socket(...);
bind(listen_fd, ...);
listen(listen_fd, ...);
pre_fork_children(...);
close(listen_fd);
wait_children_die(...);


服务器服务子进程:

while (1) {
conn_fd = accept(listen_fd, ...);
do_service(conn_fd, ...);
}


初 识上述代码,真有眼前一亮的感觉,也正如作者所说,以上代码确实很少见(反正我读此书之前是确实没见过)。作者真是构思精巧,巧妙地绕过了常见的预先创建 子进程的多进程服务器当主服务进程接收到新的连接必须想办法将这个连接传递给服务子进程的"陷阱",上述代码通过共享的倾听套接字,由子进程主动地去向内 核"索要"连接套接字,从而避免了用UNIX域套接字传递文件描述符的"淫技"。

不过,当接着往下读的时候,作者谈到了"惊群" (Thundering herd)问题。所谓的"惊群"就是,当很多进程都阻塞在accept系统调用的时候,即使只有一个新的连接达到,内核也会唤醒所有阻塞在accept上 的进程,这将给系统带来非常大的"震颤",降低系统性能。

除了这个问题,accept还必须是原子操作。为此,作者在接下来的27.7节讲述了加了互斥锁的版本:

while (1) {
lock(...);
conn_fd = accept(listen_fd, ...);
unlock(...);
do_service(conn_fd, ...);
}


原 子操作的问题算是解决了,那么"惊群"呢?文中只是提到在Solaris系统上当子进程数由75变成90后,CPU时间显著增加,并且作者认为这是因为进 程过多,导致内存互换。对"惊群"问题回答地十分含糊。通过比较书中图27.2的第4列和第7列的内容,我们可以肯定"真凶"绝对不是"内存对换"。

"元凶"到底是谁?

仔 细分析一下,加锁真的有助于"惊群"问题么?不错,确实在同一时间只有一个子进程在调用accept,其它子进程都阻塞在了lock语句,但是,当 accept返回并unlock之后呢?unlock肯定是要唤醒阻塞在这个锁上的进程的,不过谁都没有规定是唤醒一个还是唤醒多个。所以,潜在的"惊 群"问题还是存在,只不过换了个地方,换了个形式。而造成Solaris性能骤降的"罪魁祸首"很有可能就是"惊群"问题。

崩溃了!这么说所有的锁都有可能产生惊群问题了?

似乎真的是这样,所以减少锁的使用很重要。特别是在竞争比较激烈的地方。

作者在27.9节所实现的"传递文件描述符"版本的服务器就有效地克服了"惊群"问题,在现实的服务器实现中,最常用的也是此节所提到的基于"分配"形式。

把"竞争"换成"分配"是避免"惊群"问题的有效方法,但是也不要忽视"分配"的"均衡"问题,不然后果可能更加严重哦!

using AF_UNIX address family

using AF_UNIX address family

Sockets that use the AF_UNIX or AF_UNIX_CCSID address family can be connection-oriented (type SOCK_STREAM) or connectionless (type SOCK_DGRAM).

Both types are reliable because there are no external communication functions connecting the two processes.

UNIX® domain datagram sockets act differently from UDP datagram sockets. With UDP datagram sockets, the client program does not need to call the bind() API because the system assigns an unused port number automatically. The server can then send a datagram back to that port number. However, with UNIX domain datagram sockets, the system does not automatically assign a path name for the client. Thus, all client programs using UNIX domain datagrams must call the bind() API. The exact path name specified on the client's bind() is what is passed to the server. Thus, if the client specifies a relative path name (that is, a path name that is not fully qualified by starting with /), the server cannot send the client a datagram unless it is running with the same current directory.

An example path name that an application might use for this address family is /tmp/myserver or servers/thatserver. With servers/thatserver, you have a path name that is not fully qualified (no / was specified). This means that the location of the entry in the file system hierarchy should be determined relative to the current working directory.
Note: Path names in the file system are NLS-enabled.

The following figure illustrates the client/server relationship of the AF_UNIX address family.

Socket flow of events used in server and client AF_UNIX address family example programs.

Socket flow of events: Server application that uses AF_UNIX address family

The first example uses the following sequence of API calls:

  1. The socket() API returns a socket descriptor, which represents an endpoint. The statement also identifies the UNIX address family with the stream transport (SOCK_STREAM) being used for this socket. You can also use the socketpair() API to initialize a UNIX socket.

    AF_UNIX or AF_UNIX_CCSID are the only address families to support the socketpair() API. The socketpair() API returns two socket descriptors that are unnamed and connected.

  2. After the socket descriptor is created, the bind() API gets a unique name for the socket.

    The name space for UNIX domain sockets consists of path names. When a sockets program calls the bind() API, an entry is created in the file system directory. If the path name already exists, the bind() fails. Thus, a UNIX domain socket program should always call an unlink() API to remove the directory entry when it ends.

  3. The listen() allows the server to accept incoming client connections. In this example, the backlog is set to 10. This means that the system queues 10 incoming connection requests before the system starts rejecting the incoming requests.
  4. The recv() API receives data from the client application. In this example, the client sends 250 bytes of data over. Thus, the SO_RCVLOWAT socket option can be used, which specifies that recv() is not required to wake up until all 250 bytes of data have arrived.
  5. The send() API echoes the data back to the client.
  6. The close() API closes any open socket descriptors.
  7. The unlink() API removes the UNIX path name from the file system.

Socket flow of events: Client application that uses AF_UNIX address family

The second example uses the following sequence of API calls:

  1. The socket() API returns a socket descriptor, which represents an endpoint. The statement also identifies the UNIX address family with the stream transport (SOCK_STREAM) being used for this socket. You can also use the socketpair() API to initialize a UNIX socket.

    AF_UNIX or AF_UNIX_CCSID are the only address families to support the socketpair() API. The socketpair() API returns two socket descriptors that are unnamed and connected.

  2. After the socket descriptor is received, the connect() API is used to establish a connection to the server.
  3. The send() API sends 250 bytes of data that are specified in the server application with the SO_RCVLOWAT socket option.
  4. The recv() API loops until all 250 bytes of the data have arrived.
  5. The close() API closes any open socket descriptors.

creating a connectionless socket

creating a connectionless socket

Connectionless sockets do not establish a connection over which data is transferred. Instead, the server application specifies its name where a client can send requests.

Connectionless sockets use User Datagram Protocol (UDP) instead of TCP/IP.

The following figure illustrates the client/server relationship of the socket APIs used in the examples for a connectionless socket design.

The client/server relationship of the socket APIs for a connectionless protocol

Socket flow of events: Connectionless server

The following sequence of the socket calls provides a description of the figure and the following example programs. It also describes the relationship between the server and client application in a connectionless design. Each set of flows contains links to usage notes on specific APIs. If you need more details on the use of a particular API, you can use these links. The first example of a connectionless server uses the following sequence of API calls:

  1. The socket() API returns a socket descriptor, which represents an endpoint. The statement also identifies that the Internet Protocol address family (AF_INET) with the UDP transport (SOCK_DGRAM) is used for this socket.
  2. After the socket descriptor is created, a bind() API gets a unique name for the socket. In this example, the user sets the s_addr to zero, which means that the UDP port of 3555 is bound to all IPv4 addresses on the system.
  3. The server uses the recvfrom() API to receive that data. The recvfrom() API waits indefinitely for data to arrive.
  4. The sendto() API echoes the data back to the client.
  5. The close() API ends any open socket descriptors.

Socket flow of events: Connectionless client

The second example of a connectionless client uses the following sequence of API calls.

  1. The socket() API returns a socket descriptor, which represents an endpoint. The statement also identifies that the Internet Protocol address family (AF_INET) with the UDP transport (SOCK_DGRAM) is used for this socket.
  2. In the client example program, if the server string that was passed into the inet_addr() API was not a dotted decimal IP address, then it is assumed to be the host name of the server. In that case, use the gethostbyname() API to retrieve the IP address of the server.
  3. Use the sendto() API to send the data to the server.
  4. Use the recvfrom() API to receive the data from the server.
  5. The close() API ends any open socket descriptors.

creating a connection-oriented socket

creating a connection-oriented socket

These server and client examples illustrate the socket APIs written for a connection-oriented protocol such as Transmission Control Protocol (TCP).

The following figure illustrates the client/server relationship of the sockets API for a connection-oriented protocol.

The client/server relationship of the sockets API for a connection-oriented design

Socket flow of events: Connection-oriented server

The following sequence of the socket calls provides a description of the figure. It also describes the relationship between the server and client application in a connection-oriented design. Each set of flows contains links to usage notes on specific APIs.

  1. The socket() API returns a socket descriptor, which represents an endpoint. The statement also identifies that the Internet Protocol address family (AF_INET) with the TCP transport (SOCK_STREAM) is used for this socket.
  2. The setsockopt() API allows the local address to be reused when the server is restarted before the required wait time expires.
  3. After the socket descriptor is created, the bind() API gets a unique name for the socket. In this example, the user sets the s_addr to zero, which allows connections to be established from any IPv4 client that specifies port 3005.
  4. The listen() API allows the server to accept incoming client connections. In this example, the backlog is set to 10. This means that the system queues 10 incoming connection requests before the system starts rejecting the incoming requests.
  5. The server uses the accept() API to accept an incoming connection request. The accept() call blocks indefinitely, waiting for the incoming connection to arrive.
  6. The select() API allows the process to wait for an event to occur and to wake up the process when the event occurs. In this example, the system notifies the process only when data is available to be read. A 30-second timeout is used on this select()call.
  7. The recv() API receives data from the client application. In this example, the client sends 250 bytes of data. Thus, the SO_RCVLOWAT socket option can be used, which specifies that recv() does not wake up until all 250 bytes of data have arrived.
  8. The send() API echoes the data back to the client.
  9. The close() API closes any open socket descriptors.

Socket flow of events: Connection-oriented client

The following sequence of APIs calls describes the relationship between the server and client application in a connection-oriented design.

  1. The socket() API returns a socket descriptor, which represents an endpoint. The statement also identifies that the Internet Protocol address family (AF_INET) with the TCP transport (SOCK_STREAM) is used for this socket.
  2. In the client example program, if the server string that was passed into the inet_addr() API was not a dotted decimal IP address, then it is assumed to be the host name of the server. In that case, use the gethostbyname() API to retrieve the IP address of the server.
  3. After the socket descriptor is received, the connect() API is used to establish a connection to the server.
  4. The send() API sends 250 bytes of data to the server.
  5. The recv() API waits for the server to echo the 250 bytes of data back. In this example, the server responds with the same 250 bytes that was just sent. In the client example, the 250 bytes of the data might arrive in separate packets, so the recv()API can be used over and over until all 250 bytes have arrived.
  6. The close() API closes any open socket descriptors.