Socket编程实践(6) --TCP服务端注意事项

僵尸进程处理

1)通过忽略SIGCHLD信号,避免僵尸进程

在server端代码中添加

signal(SIGCHLD, SIG_IGN);

2)通过wait/waitpid方法,解决僵尸进程

signal(SIGCHLD,onSignalCatch);

void onSignalCatch(int signalNumber)
{
    wait(NULL);
}

3) 如果多个客户端同时关闭, 问题描述如下面两幅图所示:

/** client端实现的测试代码**/
int main()
{
    int sockfd[50];
    for (int i = 0; i < 50; ++i)
    {
        if ((sockfd[i] = socket(AF_INET, SOCK_STREAM, 0)) == -1)
            err_exit("socket error");

        struct sockaddr_in serverAddr;
        serverAddr.sin_family = AF_INET;
        serverAddr.sin_port = htons(8001);
        serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
        if (connect(sockfd[i], (const struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)
            err_exit("connect error");
    }
    sleep(20);
}

在客户运行过程中按下Ctrl+C,则可以看到在server端启动50个子进程,并且所有的客户端全部一起断开的情况下,产生的僵尸进程数是惊人的(此时也证明了SIGCHLD信号是不可靠的)!

解决方法-将server端信号捕捉函数改造如下:

void sigHandler(int signo)
{
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

waitpid返回值解释:

on  success,  returns the process ID of the child whose state has changed(返回已经结束运行

的子进程的PID); if WNOHANG was specified and one or more child(ren) specified by pid exist,

but have not yet changed state, then 0 is returned(如果此时尚有好多被pid参数标识的子进程存在, 并

且没有结束的迹象, 返回0).  On error, -1 is returned.

地址查询API

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);	//获取本地addr结构
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);	//获取对方addr结构

int gethostname(char *name, size_t len);
int sethostname(const char *name, size_t len);

#include <netdb.h>
extern int h_errno;
struct hostent *gethostbyname(const char *name);

#include <sys/socket.h>       /* for AF_INET */
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
struct hostent *gethostent(void);
//hostent结构体
struct hostent
{
    char  *h_name;            /* official name of host */
    char **h_aliases;         /* alias list */
    int    h_addrtype;        /* host address type */
    int    h_length;          /* length of address */
    char **h_addr_list;       /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */
/**获取本机IP列表**/
int gethostip(char *ip)
{
    struct hostent *hp = gethostent();
    if (hp == NULL)
        return -1;

    strcpy(ip, inet_ntoa(*(struct in_addr*)hp->h_addr));
    return 0;
}

int main()
{
    char host[128] = {0};
    if (gethostname(host, sizeof(host)) == -1)
        err_exit("gethostname error");

    cout << "host-name: " << host << endl;
    struct hostent *hp = gethostbyname(host);
    if (hp == NULL)
        err_exit("gethostbyname error");

    cout << "ip list: " << endl;
    for (int i = 0; hp->h_addr_list[i] != NULL; ++i)
    {
        cout << ‘\t‘
             << inet_ntoa(*(struct in_addr*)hp->h_addr_list[i]) << endl;
    }

    char ip[33] = {0};
    gethostip(ip);
    cout << "local-ip: " << ip << endl;
}

TCP协议的11种状态

1.如下图(客户端与服务器都在本机:双方(server的子进程,与client)链接已经建立(ESTABLISHED),等待通信)

2.最先close的一端,会进入TIME_WAIT状态; 而被动关闭的一端可以进入CLOSE_WAIT状态 (下图,server端首先关闭)

3.TIME_WAIT 时间是2MSL(报文的最长存活周期的2倍)

  原因:(ACK y+1)如果发送失败可以重发, 因此如果server端不设置地址重复利用的话, 服务器在短时间内就无法重启;

服务器端处于closed状态,不等于客户端也处于closed状态。

(下图, client先close, client出现TIME_WAIT状态)

4.TCP/IP协议的第1种状态:图上只包含10种状态,还有一种CLOSING状态

产生CLOSING状态的原因:

Server端与Client端同时关闭(同时调用close,此时两端同时给对端发送FIN包),将产生closing状态,最后双方都进入TIME_WAIT状态(如下图)。

SIGPIPE信号

往一个已经接收FIN的套接中写是允许的,接收到FIN仅仅代表对方不再发送数据;但是在收到RST段之后,如果还继续写,调用write就会产生SIGPIPE信号,对于这个信号的处理我们通常忽略即可。

signal(SIGPIPE, SIG_IGN);

/** 测试: 在Client发送每条信息都发送两次
当Server端关闭之后Server端会发送一个FIN分节给Client端,
第一次消息发送之后, Server端会发送一个RST分节给Client端,
第二次消息发送(调用write)时, 会产生SIGPIPE信号;
注意: Client端测试代码使用的是下节将要介绍的Socket库
**/
void sigHandler(int signo)
{
    if (SIGPIPE == signo)
    {
        cout << "receive SIGPIPE = " << SIGPIPE << endl;
        exit(EXIT_FAILURE);
    }
}
int main()
{
    signal(SIGPIPE, sigHandler);
    TCPClient client(8001, "127.0.0.1");
    try
    {
        std::string msg;
        while (getline(cin, msg))
        {
            client.send(msg);
            client.send(msg);   //第二次发送
            msg.clear();
            client.receive(msg);
            client.receive(msg);
            cout << msg << endl;
            msg.clear();
        }
    }
    catch (const SocketException &e)
    {
        cerr << e.what() << endl;
    }
}

close与shutdown的区别

#include <unistd.h>
int close(int fd);

#include <sys/socket.h>
int shutdown(int sockfd, int how);

shutdown的how参数


SHUT_RD


关闭读端


SHUT_WR


关闭写端


SHUT_RDWR


读写均关闭

1.close终止了数据传送的两个方向;

而shutdown可以有选择的终止某个方向的数据传送或者终止数据传送的两个方向。

2.shutdown how=SHUT_WR(关闭写端)可以保证对等方接收到一个EOF字符(FIN段),而不管是否有其他进程已经打开了套接字(shutdown并没采用引用计数)。

而close需要等待套接字引用计数减为0时才发送FIN段。也就是说直到所有的进程都关闭了该套接字。

示例分析:

客户端向服务器按照顺序发送:FIN E D C B A, 如果FIN是当client尚未接收到ABCDE之前就调用close发送的, 那么client端将永远接收不到ABCDE了, 而通过shutdown函数, 则可以有选择的只关闭client的发送端而不关闭接收端, 则client端还可以接收到ABCDE的信息;

/**测试: 实现与上面类似的代码(使用close/shutdown)两种方式实现 **/

完整源代码请参照:

http://download.csdn.net/detail/hanqing280441589/8486517

注意: 最好读者需要有select的基础, 没有select基础的读者可以参考我的博客<Socket编程实践(8)>相关部分

时间: 2024-10-20 08:31:45

Socket编程实践(6) --TCP服务端注意事项的相关文章

socket编程,简单多线程服务端测试程序

socket编程,简单多线程服务端测试程序 前些天重温了MSDN关于socket编程的WSAStartup.WSACleanup.socket.closesocket.bind.listen.accept.recv.send等函数的介绍,今天写了一个CUI界面的测试程序(依赖MFC)作为补充.程序功能简介如下: 1:一个线程做监听用. 2:监听线程收到客户端连接后,创建新线程接收客户端数据.所有对客户端线程将加入容器,以便管理. 3:服务端打印所有客户端发来的信息. 4:服务端CUI界面输入数字

Socket编程实践(6) --TCP粘包原因与解决

流协议与粘包 粘包的表现 Host A 发送数据给 Host B; 而Host B 接收数据的方式不确定 粘包产生的原因 说明 TCP 字节流,无边界 对等方,一次读操作,不能保证完全把消息读完 UDP 数据报,有边界 对方接受数据包的个数是不确定的 产生粘包问题的原因分析 1.SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区.接受缓冲区) 2.tcp传送的端 mss大小限制 3.链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割. 4.tcp的流量控制和拥塞控

Socket编程实践(5) --TCP粘包问题与解决

TCP粘包问题 因为TCP协议是基于字节流且无边界的传输协议, 因此非常有可能产生粘包问题, 问题描写叙述例如以下 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvempmMjgwNDQxNTg5/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" /> 对于Host A 发送的M1与M2两个各10K的数据块, Host B 接收数据的方式不确定, 有以下

python socket编程之客户端和服务端简单交互

服务端 #_*_ coding:utf-8 _*_ #导入socket模块 import socket # 创建socket对象 sk = socket.socket() #绑定侦听的IP和端口号 ip_port = ('192.168.9.213',9999) sk.bind(ip_port) #最大连接数 sk.listen(5) #接受请求,接受请求的时候可以获取到客户端的socket对象,以及客户端的IP和端口 #通过while循环,让服务端一直接受客户端请求 print "正在等待客户

Socket编程实践(9) --TCP服务器常见问题(4)

TCP/IP协议的11种状态 说明: 1.如下图(客户端与服务器都在本机:双方(server的子进程,与client)链接已经建立(ESTABLISHED),等待通信) 2.最先调用close的一端,后面会进入TIME_WAIT的状态(下图,server端首先关闭) 3.TIME_WAIT 时间是2MSL(报文的最长存活周期的2倍)     原因:(ACK y+1)如果发送失败可以重发. 服务器端处于closed状态,不等于客户端也处于closed状态.. 4.TCP/IP协议的第1种状态:图上

Socket编程实践(1) --TCP/IP简述

ISO的OSI OSI(open system interconnection)开放系统互联模型是由ISO国际标准化组织定义的网络分层模型,共七层, 从下往上为: OSI七层参考模型 物理层(Physical Layer) 物理层定义了所有电子及物理设备的规范,为上层的传输提供了一个物理介质,本层中数据传输的单位为比特(bit/二进制位).属于本层定义的规范有EIA/TIA RS-232.RJ-45等,实际使用中的设备如网卡属于本层. 数据链路层(Data Link Layer) 对物理层收到的

Socket编程实践(16) --TCP/IP各层报文(1)

以太网帧格式 说明1:链路层的数据包,称为以太网帧. 说明2:链路层不识别IP地址[因为IP地址是逻辑地址],链路层识别物理网卡MAC地址[硬件地址]. 说明3:需要根据IP地址找到对方的MAC地址(ARP地址解析协议)[MAC -> IP地址方向地址解析:RARP反向地址解析协议]. 说明4:应用层根据对等方的IP地址进行通讯,在数据封装过程中,链路层需要目的地址的MAC地址从何而来?需要将IP地址转换成MAC地址,也就是地址解析. 以太网首部代码: struct ethernet_hdr {

Socket编程实践(17) --TCP/IP各层报文(2)

UDP数据报 UDP首部代码: struct udp_hdr { unsigned short src_port; unsigned short dest_port; unsigned short len; unsigned short chksum; }; TCP报文段 协议描述 源端口号和目的端口号:源和目的主机的IP地址加上端口号构成一个TCP连接 序号和确认号:序号为该TCP数据包的第一个数据字在所发送的数据流中的偏移量:确认号为希望接收的下一个数据字的序号: 首部长度,以4个字节为单位

手写一个模块化的 TCP 服务端客户端

前面的博客 基于 socket 手写一个 TCP 服务端及客户端 写过一个简单的 TCP 服务端客户端,没有对代码结构进行任何设计,仅仅是实现了相关功能,用于加深对 socket 编程的认识. 这次我们对整个代码结构进行一下优化,使其模块化,易扩展,成为一个简单意义上的“框架”. 对于 Socket 编程这类所需知识偏底层的情况(OS 协议栈的运作机制,TCP 协议的理解,多线程的理解,BIO/NIO 的理解,阻塞函数的运作原理甚至是更底层处理器的中断.网卡等外设与内核的交互.核心态与内核态的切