最简单的回射客户/服务器程序、time_wait 状态

下面通过最简单的客户端/服务器程序的实例来学习socket API。

echoser.c 程序的功能是从客户端读取字符然后直接回射回去。

C++ Code


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

 
/*************************************************************************

> File Name: echoser.c

> Author: Simba

> Mail: [email protected]

> Created Time: Fri 01 Mar 2013 06:15:27 PM CST

************************************************************************/

#include<stdio.h>

#include<sys/types.h>

#include<sys/socket.h>

#include<unistd.h>

#include<stdlib.h>

#include<errno.h>

#include<arpa/inet.h>

#include<netinet/in.h>

#include<string.h>

#define ERR_EXIT(m) \

do { \

perror(m); \

exit(EXIT_FAILURE); \

} while (0)

int main(void)

{

int listenfd; //被动套接字(文件描述符),即只可以accept

if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)

//  listenfd = socket(AF_INET, SOCK_STREAM, 0)

ERR_EXIT("socket error");

struct sockaddr_in servaddr;

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

servaddr.sin_family = AF_INET;

servaddr.sin_port = htons(5188);

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

/* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */

/* inet_aton("127.0.0.1", &servaddr.sin_addr); */

int on = 1;

if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)

ERR_EXIT("setsockopt error");

if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)

ERR_EXIT("bind error");

if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前

ERR_EXIT("listen error");

struct sockaddr_in peeraddr; //传出参数

socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值

int conn; // 已连接套接字(变为主动套接字,即可以主动connect)

if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)

ERR_EXIT("accept error");

printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),

ntohs(peeraddr.sin_port));

char recvbuf[1024];

while (1)

{

memset(recvbuf, 0, sizeof(recvbuf));

int ret = read(conn, recvbuf, sizeof(recvbuf));

fputs(recvbuf, stdout);

write(conn, recvbuf, ret);

}

close(conn);

close(listenfd);

return 0;

}

下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。

int socket(int family, int type, int protocol);

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。

bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。struct
sockaddr
*是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。我们的程序中对myaddr参数是这样初始化的:

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

servaddr.sin_family = AF_INET;

servaddr.sin_port = htons(5188);

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

首先将整个结构体清零(也可以用bzero函数),然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为5188。

int listen(int sockfd, int backlog);

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result

argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr和addrlen参

数传NULL,表示不关心客户端的地址。

在上面的程序中我们通过peeraddr打印连接上来的客户端ip和端口号。

在while循环中从accept返回的文件描述符conn读取客户端的请求,然后直接回射回去。

echocli.c 的作用是从标准输入得到一行字符,然后发送给服务器后从服务器接收,再打印在标准输出。

C++ Code


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

 
/*************************************************************************

> File Name: echoclic

> Author: Simba

> Mail: [email protected]

> Created Time: Fri 01 Mar 2013 06:15:27 PM CST

************************************************************************/

#include<stdio.h>

#include<sys/types.h>

#include<sys/socket.h>

#include<unistd.h>

#include<stdlib.h>

#include<errno.h>

#include<arpa/inet.h>

#include<netinet/in.h>

#include<string.h>

#define ERR_EXIT(m) \

do { \

perror(m); \

exit(EXIT_FAILURE); \

} while (0)

int main(void)

{

int sock;

if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)

//  listenfd = socket(AF_INET, SOCK_STREAM, 0)

ERR_EXIT("socket error");

struct sockaddr_in servaddr;

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

servaddr.sin_family = AF_INET;

servaddr.sin_port = htons(5188);

servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

/* inet_aton("127.0.0.1", &servaddr.sin_addr); */

if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)

ERR_EXIT("connect error");

char sendbuf[1024] = {0};

char recvbuf[1024] = {0};

while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)

{

write(sock, sendbuf, strlen(sendbuf));

read(sock, recvbuf, sizeof(recvbuf));

fputs(recvbuf, stdout);

memset(sendbuf, 0, sizeof(sendbuf));

memset(recvbuf, 0, sizeof(recvbuf));

}

close(sock);

return 0;

}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。

先编译运行服务器:

[email protected]:~/Documents/code/linux_programming/UNP/socket$ ./echoser

然后在另一个终端里用netstat命令查看:

[email protected]:~$ netstat -anp | grep 5188

(Not all processes could be identified, non-owned process info

will not be shown, you would have to be root to see it all.)

tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      4425/echoser

可以看到server程序监听5188端口,IP地址还没确定下来。现在编译运行客户端:

[email protected]:~/Documents/code/linux_programming/UNP/socket$ ./echocli

回到server所在的终端,看看server的输出:

[email protected]:~/Documents/code/linux_programming/UNP/socket$ ./echoser

recv connect ip=127.0.0.1 port=59431

可见客户端的端口号是自动分配的。

再次netstat 一下

[email protected]:~$ netstat -anp | grep 5188

(Not all processes could be identified, non-owned process info

will not be shown, you would have to be root to see it all.)

tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      4425/echoser

tcp        0      0 127.0.0.1:59431         127.0.0.1:5188          ESTABLISHED 4852/echocli

tcp        0      0 127.0.0.1:5188          127.0.0.1:59431         ESTABLISHED 4425/echoser

应用程序中的一个socket文件描述符对应一个socket pair,也就是源地址:源端口号和目的地址:目的端口号,也对应一个TCP连接。

上面第一行即echoser.c 中的listenfd;第二行即echocli 中的sock; 第三行即echoser.c 中的conn。4425和4852分别是进程id。

现在来做个测试,先把echoser.c 中40~42行的代码注释起来。

首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,结果是:

[email protected]:~/Documents/code/linux_programming/UNP/socket$ ./echoser

bind error: Address already in use

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:

[email protected]:~/Documents/code/linux_programming/UNP/socket$ netstat -anp | grep 5188

(Not all processes could be identified, non-owned process info

will not be shown, you would have to be root to see it all.)

tcp        0      0 127.0.0.1:5188          127.0.0.1:37381         FIN_WAIT2   -

tcp        1      0 127.0.0.1:37381         127.0.0.1:5188          CLOSE_WAIT  2302/echocli

server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。

Many implementations prevent this infinite wait in
the FIN_WAIT_2 state as follows: If the application that does the
active close does a complete close, not a half-close indicating that it
expects
to receive data, a timer is set. If the connection is idle when the
timer expires, TCP moves the connection into the CLOSED state. In Linux,
the variable net.ipv4.tcp_fin_timeout can be adjusted to control the number of seconds
to which the timer is set. Its default value is 60s.

现在用Ctrl-C把client也终止掉,再观察现象:

[email protected]:~/Documents/code/linux_programming/UNP/socket$ netstat -anp | grep 5188

(No info could be read for "-p": geteuid()=1000 but you should be root.)

tcp        0      0 127.0.0.1:5188          127.0.0.1:37381         TIME_WAIT   -

[email protected]:~/Documents/code/linux_programming/UNP/socket$ ./echoser

bind error: Address already in use

client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum
segment lifetime)的时间后才能回到CLOSED状态,需要有MSL
时间的主要原因是在这段时间内如果最后一个ack段没有发送给对方,则可以重新发送(in which
case the other end will time out and retransmit its final FIN)。因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。

MSL: the maximum amount of time any
segment can exist in the network before being discarded. We know that
this time limit is bounded, because TCP segments are transmitted as IP
datagrams, and the IP datagram has the TTL field
or Hop Limit field that limits its effective lifetime.

Given the MSL value for an
implementation, the rule is: When TCP performs an active close and sends
the final ACK, that connection must stay in the TIME_WAIT state for
twice the MSL. This lets TCP resend the final ACK in case
it is lost. The final ACK is resent not because the TCP retransmits ACKs
(they do not consume sequence numbers and are not retransmitted by
TCP), but because the other side will retransmit its FIN (which does
consume a sequence
number). Indeed, TCP will always retransmit FINs until it receives a
final ACK.

在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开指的是connfd(127.0.0.1:5188)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:5188),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard

address(比如一台机器可能有内网和外网两张网卡)。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。将原来注释的40~42行代码打开,问题解决。

参考:

《Linux C 编程一站式学习》

《TCP/IP详解 卷一》

《UNP》

http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html

http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html

原文地址:https://www.cnblogs.com/alantu2018/p/8472843.html

时间: 2024-08-24 04:05:55

最简单的回射客户/服务器程序、time_wait 状态的相关文章

第十一篇:基于TCP的一对回射客户/服务器程序及其运行过程分析( 下 )

执行分析 1. 打开服务器进程: 2. 执行netstat -a命令观察当前的连接状态: 第1条连接记录说明:绑定了本地主机的任意IP,端口为9877,目前处于监听状态. 3. 打开客户进程: 4. 执行netstat -a命令观察当前的连接状态,发现了两个新的连接: 以及 上面一个连接说明一个连接到服务器的连接,客户端临时端口是32818,目的端口正是先前的9877,连接状态为已建立,对应已连接套接字: 下面一个连接说明一个连接到客户端的连接,服务端端口为9877,目的端口是32818,这个连

第十篇:基于TCP的一对回射客户/服务器程序及其运行过程分析( 上 )

前言 本文将讲解一对经典的客户/服务器回射程序,感受网络编程的大致框架( 该程序稍作改装即可演变成各种提供其他服务的程序 ):同时,还将对其运行过程加以分析,观察程序背后协议的执行细节,学习调试网络程序的技巧. 客户端 1 #include "unp.h" 2 3 void str_cli(FILE *fp, int sockfd); 4 5 int 6 main(int argc, char **argv) 7 { 8 int sockfd; 9 struct sockaddr_in

利用System V消息队列实现回射客户/服务器

一.介绍 在学习UNIX网络编程 卷1时,我们当时可以利用Socket套接字来实现回射客户/服务器程序,但是Socket编程是存在一些不足的,例如: 1. 服务器必须启动之时,客户端才能连上服务端,并与服务端进行通信: 2. 利用套接口描述符进行通信,必须知道对端的IP与端口. 二.相关函数介绍 下面,我们利用System V消息队列来实现进程间的通信: 首先,我们先来了解一下下面几个函数: 1. msgget: 该函数用于打开或创建消息队列,其作用相当与文件操作函数open. #include

简单回射客户/服务器

一.程序功能 (1)客户从标准输入读入一行文本行,并写给服务器: (2)服务器从网络输入读入这行文本,并回射给客户: (3)客户从网络输入读入这行回射文本,并显示在标准输出上 二.服务器程序 #include <stdio.h> #include <stdlib.h> #include <time.h> #include <errno.h> #include <string.h> #include <sys/types.h> #inc

UNIX网络编程卷1 回射客户程序 TCP客户程序设计范式

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 下面我会介绍同一个使用 TCP 协议的客户端程序的几个不同版本,分别是停等版本.select 加阻塞式 I/O 版本. 非阻塞式 I/O 版本.fork 版本.线程化版本.它们都由同一个 main 函数调用来实现同一个功能,即回射程序客户端. 它从标准输入读入一行文本,写到服务器上,读取服务器对该行的回射,并把回射行写到标准输出上. 其中,非阻塞式 I/O 版本是所有版本中执行速度最快的,

socket编程(一):简单回射C/S程序

服务端代码: //echoSvr.c #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <str

TCP客户/服务器程序概述

一个回射服务器: 1)客户从标准输入读入一行文本,并写给服务器 2)服务器从网络输入读入这行文本,并回射给客户 3)客户从网络输入读入这行回射文本,并显示在标准输出上 回射输入行这样一个客户/服务器程序是一个尽管简单然而有效的网络程序例子 实现任何客户/服务器网络应用所需的所有基本步骤可通过本例子阐述 若想把本例子扩充成你自己的应用程序,你只需修改服务器对来自客户的输入的处理过程 除了以正常的方式运行本例子的客户和服务器(即键入一行文本并观察它的回射)之外 我们还会讨论它的许多边界条件: 客户和

UNIX网络编程笔记(4)—TCP客户/服务器程序示例

TCP客户/服务器程序示例 这一章信息量开始大起来了,粗略来看它实现了简单的TCP客户/服务器程序,里面也有一些费解的细节. 1.概述 完整的TCP客户/服务器程序示例.这个简单的例子将执行如下步骤的一个回射服务器(这里的回射服务器就是服务简单的把客户端发送的消息返回给客户): 1)客户从标准输入读入一行文本,并写给服务器 2)服务器从网络输入读入这行文本,并回射给客户 3)客户从网络输入读入这行回射文本,并显示在标准输出上 这样实际上就构成了一个全双工的TCP连接. 本章就围绕了这个简单的TC

【UNIX网络编程】TCP客户/服务器程序示例

做一个简单的回射服务器: 客户从标准输入读入一行文本,写给服务器 -> 服务器从网络输入读入这行文本,并回射给客户 -> 客户从网络输入读入这行回射文本,并显示在标准输出上 以下是我的代码(部分.h文件是由unpv13e文件夹中的.c文件改名得到) #include "../unpv13e/unp.h" #include "../unpv13e/apueerror.h" #include "../unpv13e/wrapsock.h"