好教程推荐系列:TCP面试常见题-张小方的知乎Live-轻松搞定技术面试中常见的网络通信问题

先声明一下,文章是我自己整理的笔记,内容是张小方先生的讲稿。欢迎关注张先生主持的【高性能服务器开发】WeChat gongzhong号。

张先生的博客地址是http://blog.csdn.net/analogous_love

张小方 的知乎 Live:轻松搞定技术面试中常见的网络通信问题https://www.zhihu.com/lives/922110858308485120?utm_source=qq&utm_medium=social

本live列举的这些网络问题涵盖了技术面试中关于网络通信的80%的面试题,深入理解并掌握这些原理,不仅能帮您写出高质量的网络通信程序,同时能应付面试中绝大多数网络通信题目,助您的升职加薪一臂之力。

 

本次 Live 主要包括以下内容 • TCP/IP协议栈层次与三次握手、四次挥手需要知道的细节 • TCP与UDP适用场景 • linux网络模型 • epoll_event结构中epoll_data_t的fd与ptr使用场景 •Windows网络模型 •异步connect •select可以检测网络异常吗 •epoll的水平模式和边缘模式 •阻塞与非阻塞socket的设置与区别 •send/recv返回值问题 •如何编写正确的收与发数据代码 •收发缓冲区如何设计 •SO_SNDTIMEO、SO_RCVTIMEO、TCP_NODELAY、SO_REUSEADDR和SO_REUSEPORT、SO_LINGER选项 •shutdown与优雅关闭 •错误码EINTR •tcp粘包问题 •信号SIGPIPE与EPIPE错误码 •gethostbyname阻塞与错误码 •SO_KEEPALIVE选项与心跳包设计技巧 •如何设计断线重连机制 •如何清除无效的死链 •网络框架中定时器不同实现 •http协议格式、head、get与post方法细节 •http、socks4与socks5代理编码实现 •你问我答互动环节 •总结 

 

技术面试中常见的网络通信细节问题解答

1. TCP/IP协议栈层次结构

2. TCP三次握手需要知道的细节点

3. TCP四次挥手需要知道的细节点(CLOSE_WAIT、TIME_WAIT、MSL)

4. TCP与UDP的区别与适用场景

5. linux常见网络模型详解(select、poll与epoll)

6. epoll_event结构中的epoll_data_t的fd与ptr的使用场景

7. Windows常见的网络模型详解(select、WSAEventSelect、WSAAsyncSelect)

8. Windows上的完成端口模型(IOCP)

9. 异步的connect函数如何编写

10.select函数可以检测网络异常吗?

11.你问我答环节一

12. epoll的水平模式和边缘模式

13. 如何将socket设置成非阻塞的(创建时设置与创建完成后设置),非阻塞socket与阻塞的socket在收发数据上的区别

14. send/recv(read/write)返回值大于0、等于0、小于0的区别

15.如何编写正确的收数据代码与发数据代码

16.发送数据缓冲区与接收数据缓冲区如何设计

17.socket选项SO_SNDTIMEO和SO_RCVTIMEO

18.socket选项TCP_NODELAY

19.socket选项SO_REUSEADDR和SO_REUSEPORT(Windows平台与linux平台的区别)

20.socket选项SO_LINGER

21.shutdown与优雅关闭

22.你问我答环节二

23.socket选项SO_KEEPALIVE

24.关于错误码EINTR

25.如何解决tcp粘包问题

26.信号SIGPIPE与EPIPE错误码

27.gethostbyname阻塞与错误码获取问题

28.心跳包的设计技巧(保活心跳包与业务心跳包)

29.断线重连机制如何设计

30.如何检测对端已经关闭

31.如何清除无效的死链(端与端之间的线路故障)

32.定时器的不同实现及优缺点

33.你问我答环节三

34.http协议的具体格式

35.http head、get与post方法的细节

36.http代理、socks4代理与socks5代理如何编码实现

37.ping

38.telnet

39.你问我答环节四

40.总结

 

--------------------------------------------------------------------------------------

 

 

技术面试中常见的网络通信细节问题解答

1. TCP/IP协议栈层次结构

 

2. TCP三次握手需要知道的细节点

答:1、三次握手,如果前两次有某一次失败,会重新从第一次开始,重来三次。

2、三次握手,如果最后一次失败,服务器并不会重传ack报文,而是直接发送RTS报文段,进入CLOSED状态。这样做的目的是为了防止SYN洪泛攻击。详情参见http://blog.csdn.net/libaineu2004/article/details/79020031

3、发起连接时如果发生TCP SYN丢包,那么系统默认的重试间隔是3s,这期间不会返回错误码。

4、如何模拟tcp挥手失败?答案是iptables命令可以过滤数据包,丢弃所有的连接请求,致使客户端无法得到任何ack报文。

TCP 的那些事儿(上)
TCP 的那些事儿(下)

 

3. TCP四次挥手需要知道的细节点(CLOSE_WAIT、TIME_WAIT、MSL)

答:http://blog.csdn.net/libaineu2004/article/details/78886213 再谈应用环境下的TIME_WAIT和CLOSE_WAIT

http://blog.csdn.net/libaineu2004/article/details/78886182 CLOSE_WAIT状态的原因与解决方法

http://blog.csdn.net/libaineu2004/article/details/78803068 TCP面试常见题:time_wait状态产生的原因,危害,如何避免

 

4. TCP与UDP的区别与适用场景

答:TCP协议栈本身是可靠,不会丢包,不会乱序,失败会重发。UDP需要应用层做协议来保证可靠性。视频可以用UDP。

必须使用udp的场景:广播。

 

5. linux常见网络模型详解(select、poll与epoll)

答:select和poll本质上没有区别,都是轮询,但是poll没有最大设备描述符数量的限制。

 

6. epoll_event结构中的epoll_data_t的fd与ptr的使用场景

答:在epoll模型中使用了一个struct epoll_event的结构体: 

typedef union epoll_data 

{ void *ptr;

 int fd; 

 uint32_t u32;

 uint64_t u64;

 } epoll_data_t; 

struct epoll_event 

{ uint32_t events; /* Epoll events */ 

 epoll_data_t data; /* User data variable */

 };

 

7. Windows常见的网络模型详解(select、WSAEventSelect、WSAAsyncSelect)

8. Windows上的完成端口模型(IOCP)

9. 异步的connect函数如何编写

参考博客:http://blog.csdn.net/analogous_love/article/details/60761528

 

//关于tcp连接的异步connect实现流程如下:  
//(1)设置socket连接为非阻塞.  
//(2)调用connect函数.返回0表明连接成功.如果返回-1,同时errno为EINPROGRESS表明正在建立连接.  
//(3)使用select , epoll等 , 当描述符可写的时候检查连接状态.  
  
#include <stdio.h>  
#include <fcntl.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <errno.h>  
#include <sys/epoll.h>  
#include <netinet/in.h>  
#include <string.h>  
  
void setnonblock(int fd)  
{  
    int flags = fcntl(fd, F_GETFL);  
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);  
}  
  
int main(){  
    const char* ip = "127.0.0.1";  
    short port = 9999;  
      
    //设置连接地址  
    struct sockaddr_in addr;  
    socklen_t socklen = sizeof(struct sockaddr_in);  
    memset(&addr , 0 , sizeof(struct sockaddr_in));  
    addr.sin_family = AF_INET;  
    addr.sin_addr.s_addr = inet_addr(ip);  
    addr.sin_port = htons(port);  
      
    //创建描述符  
    int fd = socket( AF_INET , SOCK_STREAM , 0);  
    if (fd < 0){  
        printf("socket() error\n");  
        return -1;  
    }  
    //设置描述符为非阻塞  
    setnonblock(fd);  
      
    //连接  
    int res;  
    res = connect(fd , (struct sockaddr*)&addr , socklen);  
    if (res == 0){  
        printf("connect ok(1)\n");  
    } else if (res == -1 && errno != EINPROGRESS){  
        printf("connect err(1)\n");  
        close(fd);  
        return -1;  
    } else {  
        int epfd;  
        //创建epoll描述符  
        epfd = epoll_create(1024);  
        if ( (epfd = epoll_create(1024) ) == -1){  
            printf("epoll_create() err\n");  
            close(fd);  
            return -1;  
        }  
        //添加关注事件  
        struct epoll_event ev;  
        ev.events = EPOLLOUT;  
        ev.data.fd = fd;  
        epoll_ctl( epfd , EPOLL_CTL_ADD , fd , &ev);  
          
        //编写网络程序的时候,epoll是程序的主循环.我们这里为了测试,连接上或connect超时(75秒)就break掉.  
        //正常的流程是写一个处理connect结果的回调函数.  
        int event_max = 1;  
        struct epoll_event events[event_max];  
        int i;  
        while (1){  
            res = epoll_wait( epfd , events , event_max , -1);  
            if (res > 0){  
                for ( i = 0 ; i < res ; i++){  
                    if ( events[i].data.fd == fd && ( events[i].events & EPOLLOUT) ){ //29(EPOLLOUT|EPOLLERR|EPOLLHUP)  //4(EPOLLOUT)  
                        //检查是否连接成功  
                        int optval;  
                        socklen_t optlen = sizeof(optval);  
                        int res1 = getsockopt(fd, SOL_SOCKET, SO_ERROR, &optval, &optlen);  
                        if ( res1 < 0 || optval){  
                            close(fd);  
                            close(epfd);  
                            printf("connect err(2)\n");  
                            return -1;  
                        } else {  
                            printf("connect ok(2)\n");  
                        }  
                    }  
                }  
                break;  
            }  
        }  
        close(fd);  
        close(epfd);  
    }  
}  

 

10.select函数可以检测网络异常吗?

答:不可以。当网络异常时,select函数可以检测到可读事件,这时候用read函数读取数据,会返回0.

详情见UNP 卷1 6.3.1 select描述符就绪条件 第131页和132页

 

12. epoll的水平模式LT和边缘模式ET

答:默认是LT。

水平模式指的是低电平到低电平,或者高电平到高电平。

边缘模式指的是低电平到高电平,或者高电平到低电平。

水平模式:如果epoll_wait检测到可读事件,可以一次性把数据读完,也可以分多次读完。

边缘模式:如果epoll_wait检测到可读事件,必须一次性把数据读完,否则如果分两次读,第一次读部分,第二次再读时,没有可读触发信号了,读不到了。只能使用非阻塞的网络模型。

参考博客:http://blog.csdn.net/analogous_love/article/details/60761528

用于windows或linux水平模式下收取数据,这种情况下收取的数据可以小于指定大小,总之一次能收到多少是多少:

bool TcpSession::Recv()  
{  
    //每次只收取256个字节  
    char buff[256];  
    //memset(buff, 0, sizeof(buff));  
    int nRecv = ::recv(clientfd_, buff, 256, 0);  
    if (nRecv == 0)  
        return false;  
  
    inputBuffer_.add(buff, (size_t)nRecv);  
  
    return true;  
}  

 

如果是linux epoll边缘模式(ET),则一定要一次性收完:

bool TcpSession::RecvEtMode()  
{  
    //每次只收取256个字节  
    char buff[256];  
    while (true)  
    {  
        //memset(buff, 0, sizeof(buff));  
        int nRecv = ::recv(clientfd_, buff, 256, 0);  
        if (nRecv == -1)  
        {  
            if (errno == EWOULDBLOCK || errno == EINTR)  
                return true;  
  
            return false;  
        }  
        //对端关闭了socket  
        else if (nRecv == 0)  
            return false;  
          
       inputBuffer_.add(buff, (size_t)nRecv);  
    }  
  
    return true;  
}  

 

13. 如何将socket设置成非阻塞的(创建时设置与创建完成后设置),非阻塞socket与阻塞的socket在收发数据上的区别

14. send/recv(read/write)返回值大于0、等于0、小于0的区别

答:

recv:
阻塞与非阻塞recv返回值没有区分,都是 <0:出错,=0:连接关闭,>0接收到数据大小,
特别:非阻塞模式下返回 值 <0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况 下认为连接是正常的,继续接收。
只是阻塞模式下recv会阻塞着接收数据,非阻塞模式下如果没有数据会返回,不会阻塞着读,因此需要 循环读取。
write:
阻塞与非阻塞write返回值没有区分,都是 <0:出错,=0:连接关闭,>0发送数据大小,
特别:非阻塞模式下返回值 <0时并且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的, 继续发送。
只是阻塞模式下write会阻塞着发送数据,非阻塞模式下如果暂时无法发送数据会返回,不会阻塞着 write,因此需要循环发送。
read:
阻塞与非阻塞read返回值没有区分,都是 <0:出错,=0:连接关闭,>0接收到数据大小,
特别:非阻塞模式下返回 值 <0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况 下认为连接是正常的,继续接收。
只是阻塞模式下read会阻塞着接收数据,非阻塞模式下如果没有数据会返回,不会阻塞着读,因此需要 循环读取。
send:
阻塞与非阻塞send返回值没有区分,都是 <0:出错,=0:连接关闭,>0发送数据大小,
特别:非阻塞模式下返回值 <0时并且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的, 继续发送。
只是阻塞模式下send会阻塞着发送数据,非阻塞模式下如果暂时无法发送数据会返回,不会阻塞着 send,因此需要循环发送。

 

15.如何编写正确的收数据代码与发数据代码

答:都需要考虑缓冲区的设计,发数据如果失败先缓存起来,等待下一次的机会再发。

参考博客:http://blog.csdn.net/analogous_love/article/details/60761528

bool TcpSession::Send()  
{  
    while (true)  
    {  
        int n = ::send(clientfd_, buffer_, buffer_.length(), 0);  
        if (n == -1)  
        {  
            //tcp窗口容量不够, 暂且发不出去,下次再发  
            if (errno == EWOULDBLOCK)  
                break;  
            //被信号中断,继续发送  
            else if (errno == EINTR)  
                continue;  
  
            return false;  
        }  
        //对端关闭了连接  
        else if (n == 0)  
            return false;  
  
        buffer_.erase(n);  
        //全部发送完毕  
        if (buffer_.length() == 0)  
            break;  
    }  
  
    return true;  
}  

tcp是流协议,应用层要自己来区分包的边界,就是加包头包尾。但是tcp有个滑动窗口,假如是10,我第一个数据包占了6,然后在服务器还没读取的时候,我发送第二个,也是6大小,此时会先只发4过去,剩下的6-4=2就得等服务端腾出空间后并提示后再发了.

 

16.发送数据缓冲区与接收数据缓冲区如何设计

答:参考学习muduo的buffer设计

 

17.socket选项SO_SNDTIMEO和SO_RCVTIMEO

答:阻塞模式时需要设置超时时间,否则会卡死。

 

18.socket选项TCP_NODELAY

答:一般来说,应用层send函数不会立刻把数据发出去,而是先给到网卡缓冲区。网卡缓冲区需要等待数据积累到一定量之后才会发送数据,这样会导致一定的延迟。

默认情况下,发送数据采用Nagle算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Nagle算法,避免连续发包出现延迟,这对低延迟网络服务很重要。 此时,应用程序向内核递交的每个数据包都会立即发送出去。需要注意的是,虽然禁止了Nagle 算法,但网络的传输仍然受到TCP确认延迟机制的影响。

 

19.socket选项SO_REUSEADDR和SO_REUSEPORT(Windows平台与linux平台的区别)

答:说白了当服务器进程关闭时,想立刻再复用原来的ip和端口需要等待2MSL的时间。举个例子,服务器监听了127.0.0.1和8001端口,如果此时结束掉进程,再立刻重启,是不可以再监听成功的。因为TCP四次挥手最后一步TIME_WAIT需要等待应答,如果等不到需要重连。

MSL的时间一般是1min~4min不等。MSL是数据包的最大存活时间,最后一步的ack需要考虑去和回,所以周期是2*MSL。

Linux是所有进程在2MSL的时间内不能复用刚才使用的ip和port,bind会失败;Windows是除了本进程可以,其他进程不可以。操作系统这么设计的。

结论:一般为了方便重启服务器或调试,会设置这两个选项,REUSE就是复用的意思,让进程立刻可以复用地址和端口。

 

20.socket选项SO_LINGER

答:当调用closesocket关闭套接字时,SO_LINGER将决定系统如何处理残存在套接字发送队列中的数据。处理方式无非两种:丢弃或者将数据继续发送至对端,优雅关闭连接。事实上,SO_LINGER并不被推荐使用,大多数情况下我们推荐使用默认的关闭方式(即下方表格中的第一种情况)。

 

21.shutdown与优雅关闭

答:socket 多进程中的shutdown, close使用
当所有的数据操作结束以后,你可以调用close()函数来释放该socket,从而停止在该socket上的任何数据操作:close(sockfd);
你也可以调用shutdown()函数来关闭该socket。该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某socket的写操作而允许继续在该socket上接受数据,直至读入所有数据。
int shutdown(int sockfd,int how);
Sockfd是需要关闭的socket的描述符。参数 how允许为shutdown操作选择以下几种方式:
    SHUT_RD:关闭连接的读端。也就是该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。进程将不能对该套接字发出任何读操作。对TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。
    SHUT_WR:关闭连接的写端,进程不能在对此套接字发出写操作。
    SHUT_RDWR:相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。

服务器如果要主动关闭连接,可以这么执行:先关本地“写”端,等对方关闭后,再关本地“读”端。

服务器如果要被动关闭连接,可以这么执行:当read函数返回值是0时,先关本地“写”端,等对方关闭后,再关本地“读”端。

 

23.socket选项SO_KEEPALIVE

答:一般来说不推荐使用默认的心跳机制,默认是2小时。默认心跳有两个缺陷:

1、貌似设置之后会影响整个操作系统所有应用层的心跳时间;

2、每2小时发一次心跳,有时候会造成流量浪费。比如应用层如果有正常数据交互,不需要发心跳。

具体实现可以参考redis源码anet.c里的anetKeepAlive函数。

 

24.关于错误码EINTR

答:EINTR是linux中函数的返回状态,在不同的函数中意义不同。表示某种阻塞的操作,被接收到的信号中断,造成的一种错误返回值。
write
表示:由于信号中断,没写成功任何数据。
read
表示:由于信号中断,没读到任何数据。
sem_wait
函数调用被信号处理函数中断。
recv
由于信号中断返回,没有任何数据可用。

 

25.如何解决tcp粘包问题

答:通过应用层自定义协议来解决

1、固定长度的包

2、每个包以"\r\n"结尾

3、定义结构体,包含固定包头,包体长度等

 

26.信号SIGPIPE与EPIPE错误码

答:在linux下写socket的程序的时候,如果服务器尝试send到一个disconnected socket上,就会让底层抛出一个SIGPIPE信号。 这个信号的缺省处理方法是退出进程,大多数时候这都不是我们期望的。也就是说,当服务器繁忙,没有及时处理客户端断开连接的事件,就有可能出现在连接断开之后继续发送数据的情况,如果对方断开而本地继续写入的话,就会造成服务器进程意外退出。

根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以client会退出。若不想客户端退出可以把 SIGPIPE设为SIG_IGN 如:signal(SIGPIPE, SIG_IGN); 这时SIGPIPE交给了系统处理。 服务器采用了fork的话,要收集垃圾进程,防止僵尸进程的产生,可以这样处理: signal(SIGCHLD,SIG_IGN); 交给系统init去回收。 这里子进程就不会产生僵尸进程了。

 

27.gethostbyname阻塞与错误码获取问题

答:Unix/Linux下的gethostbyname函数常用来向DNS查询一个域名的IP地址。 由于DNS的递归查询,常常会发生gethostbyname函数在查询一个域名时严重超时。而该函数又不能像connect和read等函数那样通过setsockopt或者select函数那样设置超时时间,因此常常成为程序的瓶颈。有人提出一种解决办法是用alarm设置定时信号,如果超时就用setjmp和longjmp跳过gethostbyname函数(这种方式我没有试过,不知道具体效果如何)。
gethostbyname确实是阻塞的,但应该可以设置一个time_out免得DNS Server出问题时老是执行,关于设置Time_out,参阅一下code:

int timeout = TIMEOUT_VALUE; 
int err; 
SOCKET s; 
s = socket( ... ); 
err = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout).

 

在使用 gethostbyname() 的时候,你不能用perror() 打印错误信息 (因为 errno 没有使用),你应该调用 herror()。herror()函数签名如下:
void herror(const char *s);

举例如下:参考博客http://blog.csdn.net/analogous_love/article/details/53433994

bool Connect(const char* pszIp, int nPort)  
{  
    struct hostent* pHostent = NULL;  
    char* host = NULL;  
  
    struct sockaddr_in addrSrv;  
    memset(&addrSrv, 0, sizeof(addrSrv));  
    addrSrv.sin_addr.s_addr = ::inet_addr(pszIp);  
    if (addrSrv.sin_addr.s_addr == INADDR_NONE)  
    {  
        pHostent = ::gethostbyname(pszIp);  
        if (pHostent != NULL)  
        {  
            host = inet_ntoa(*((struct in_addr *)pHostent->h_addr));  
            std::cout << pszIp << "=" << host << std::endl;  
        }  
        else  
        {  
            herror("gethostbyname error");  
            std::cout << std::endl;  
            return false;  
        }  
    }  
    else  
        host = (char*)pszIp;  
  
    if (Socket.Connect(host, nPort))  
        return true;  
  
    std::cout << "Unable to connect to server " << pszIp << ":" << nPort << std::endl;  
    return false;  
}

 

28.心跳包的设计技巧(保活心跳包与业务心跳包)

答:http://blog.csdn.net/analogous_love/article/details/78388187

 

29.客户端断线重连机制如何设计

答:客户端先2s连接一次服务器,如果失败,再4s连接一次,如果失败,再8s连接一次,如果失败再16s连接一次。。。。。

补充一个情况,当网络状况突变时,立刻连接一次。例如,用户从地铁站出来,手机信号满格了,此时手机app立刻连接服务器。

《Linux多线程服务端编程:使用muduo C++网络库》P333有这么描述:

客户端连接断开后初次重试的延迟应该有随机性,比如说服务端奔溃,它所有的客户连接同时断开,然后0.5s之后再次发起连接,这样既可能造成SYN丢包,也可能给服务器带来短期大负载,影响其服务质量。因此每个客户端应该等待一段随机的时间(0.5~2s),再重试,避免拥塞。

 

30.如何检测对端已经关闭socket

答:根据ERRNO和recv结果进行判断
在UNIX/LINUX下,非阻塞模式SOCKET可以采用recv+MSG_PEEK的方式进行判断,其中MSG_PEEK保证了仅仅进行状态判断,而不影响数据接收
对于主动关闭的SOCKET, recv返回-1,而且errno被置为9(#define EBADF   9 /* Bad file number */)或104 (#define ECONNRESET 104 /* Connection reset by peer */)
对于被动关闭的SOCKET,recv返回0,而且errno被置为11(#define EWOULDBLOCK EAGAIN /* Operation would block */)
 对正常的SOCKET, 如果有接收数据,则返回>0, 否则返回-1,而且errno被置为11(#define EWOULDBLOCK EAGAIN /* Operation would block */)
因此对于简单的状态判断(不过多考虑异常情况):
    recv返回>0,   正常

 

31.如何清除无效的死链(端与端之间的线路故障)

答:TCP四次挥手时产生的TIME_WAIT或CLOSE_WAIT,造成死链。或者服务器A<->路由器B<->路由器C<->客户端D,链路中的路由器发生了故障,造成死链。需要采取定时器/心跳检测来清理死链。

 

32.定时器的不同实现及优缺点

答:Windows可以使用OnTimer函数,Linux网络库定时器需要自己实现。

例如libevent的小根堆,libuv的红黑树,muduo的二叉搜索树,nginx的红黑树,redis的升序链表等

学习redis网络库,muduo,优先队列std:priority_queue

 

34.http协议的具体格式

35.http head、get与post方法的细节

答:

GET /index.php HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
content-length: 8
User-Agent: Mozilla/5.0\r\n
\r\n
abcdefgh
POST /index.php HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
content-length: 8
User-Agent: Mozilla/5.0\r\n
\r\n
abcdefgh

 

 

36.http代理、socks4代理与socks5代理如何编码实现

答:代码如下

BOOL UMySocket::Connect(PNETCONN_INFO pInfo,LPSTR lpMessage) //连接服务器
{
wsysplus_memory	vMemory;
long noDelay(1),tmSend(1800*1000L),tmRecv(1800*1000L);
LPSTR lpBuffer=vMemory.GetBuf(8001);
//关闭连接先
UMySocket::Close();
//初始化
if(!lpBuffer)
{
if(lpMessage) strcpy(lpMessage,"MALLOC DATA");
return(FALSE);
}
_hSocket=socket(PF_INET,SOCK_STREAM,0);
if(_hSocket==INVALID_SOCKET)
{
if(lpMessage) strcpy(lpMessage,"INVALID_SOCKET");
return(FALSE);
}
//设置连接属性
setsockopt(_hSocket,IPPROTO_TCP,TCP_NODELAY,(LPSTR)&noDelay,sizeof(long));
setsockopt(_hSocket,SOL_SOCKET,SO_SNDTIMEO,(LPSTR)&tmSend,sizeof(long));
setsockopt(_hSocket,SOL_SOCKET,SO_RCVTIMEO,(LPSTR)&tmRecv,sizeof(long));
//连接服务器
WORD wPortConn=(WORD)pInfo->proxyport;
char *lpServer=pInfo->proxyurl;
sockaddr_in	remote={0};
remote.sin_family = AF_INET;
if(!(pInfo->conntype&0x000F)) //未使用代理
{
lpServer = pInfo->srvurl;
wPortConn = (WORD)pInfo->srvport;
}

remote.sin_port = htons(wPortConn); 
LPHOSTENT lphost=NULL; 
if((remote.sin_addr.s_addr=inet_addr(lpServer))==INADDR_NONE) 
{ 
if(lphost=gethostbyname(lpServer)) 
remote.sin_addr.s_addr = ((LPIN_ADDR)lphost->h_addr)->s_addr; 
} 
do 
{ 
if(connect(_hSocket,(sockaddr*)&remote,sizeof(remote)) 
==SOCKET_ERROR&&WSAGetLastError()!=WSAEWOULDBLOCK) 
{ 
if(lpMessage) sprintf(lpMessage,"服务器:%s:%d连接失败",lpServer,wPortConn); 
break; 
} 
if(pInfo->conntype&PROXY_HTTP)//http proxy 
{ 
sprintf(lpBuffer,"CONNECT %s:%d HTTP/1.0\r\nUser-Agent:rmtcmd/0.1\r\n\r\n", 
pInfo->srvurl,pInfo->srvport); 
send(_hSocket,lpBuffer,strlen(lpBuffer),0); 
if(recv(_hSocket,lpBuffer,8000,0)<1) 
{ 
if(lpMessage) sprintf(lpMessage,"代理服务器:%s:%d通讯失败",pInfo->srvurl,pInfo->srvport); 
break; 
} 
if(strstr(lpBuffer,"Connection established")==NULL) 
{ 
if(lpMessage) sprintf(lpMessage,"代理服务器:%s:%d通讯失败",pInfo->srvurl,pInfo->srvport); 
break; 
} 
}
else if(pInfo->conntype&PROXY_SOCK5) //sock5 proxy 
{ 
PSOCK5REQ req = (PSOCK5REQ)lpBuffer; 
PSOCK5ANS ans = (PSOCK5ANS)(lpBuffer+1024); 
req->ver = 5; 
req->lmethods = 2; 
req->methods[0] = 0; 
req->methods[1] = 2; 
send(_hSocket,(LPSTR)req,4,0); 
if(!Recv(ans,sizeof(SOCK5ANS))|| 
ans->ver!=5||(ans->method&&ans->method!=2)) 
{ 
if(lpMessage) sprintf(lpMessage,"代理服务器:%s:%d通讯失败",pInfo->srvurl,pInfo->srvport); 
break; 
} 
if(ans->method==2)	//need user & passwd 
{ 
PAUTHREQ reqa = (PAUTHREQ)lpBuffer; 
PAUTHANS ansa = (PAUTHANS)(lpBuffer+1024); 
memset(reqa,0,sizeof(AUTHREQ)); 
reqa->ver = 1; 
reqa->ulen = strlen(strcpy(reqa->user,pInfo->proxyuser)); 
reqa->plen = strlen(strcpy(reqa->passwd,pInfo->proxypasswd)); 
send(_hSocket,lpBuffer,sizeof(AUTHREQ),0); 
if(!Recv(ansa,sizeof(AUTHANS))||ansa->ver!=1||ansa->status) 
{ 
if(lpMessage) sprintf(lpMessage,"代理服务器:%s:%d通讯失败",pInfo->srvurl,pInfo->srvport); 
break; 
} 
}
PSOCK5REQEX reqex = (PSOCK5REQEX)lpBuffer; 
PSOCK5ANSEX ansex = (PSOCK5ANSEX)(lpBuffer+1024); 
memset(reqex,0,sizeof(SOCK5REQEX)); 
reqex->ver = 5; 
reqex->cmd = 1; 
reqex->rsv = 0; 
reqex->atyp = 1; 
if((reqex->addr=inet_addr(pInfo->srvurl))==INADDR_NONE) 
{ 
if(lphost=gethostbyname(pInfo->srvurl)) 
reqex->addr = ((LPIN_ADDR)lphost->h_addr)->s_addr; 
} 
reqex->port = ntohs((WORD)pInfo->srvport); 
send(_hSocket,(LPSTR)reqex,sizeof(SOCK5REQEX),0); 
if(!Recv(ansex,sizeof(SOCK5ANSEX))||ansex->ver!=5||ansex->rep) 
{ 
if(lpMessage) sprintf(lpMessage,"代理服务器:%s:%d通讯失败",pInfo->srvurl,pInfo->srvport); 
break; 
} 
} 
else if(pInfo->conntype&PROXY_SOCK4) //sock4 proxy 
{ 
PSOCK4REQ req=(PSOCK4REQ)lpBuffer; 
PSOCK4ANS ans=(PSOCK4ANS)(lpBuffer+1024); 
req->vn = 4; 
req->cd = 1; 
req->port = ntohs((WORD)pInfo->srvport); 
if((req->addr=inet_addr(pInfo->srvurl))==INADDR_NONE) 
{ 
if(lphost=gethostbyname(pInfo->srvurl)) 
req->addr = ((LPIN_ADDR)lphost->h_addr)->s_addr; 
}
send(_hSocket,lpBuffer,sizeof(SOCK4REQ),0); 
if(!Recv(ans,sizeof(SOCK4ANS))) 
{ 
if(lpMessage) sprintf(lpMessage,"代理服务器:%s:%d通讯失败",pInfo->srvurl,pInfo->srvport); 
break; 
} 
if(ans->vn||ans->cd!=90) 
{ 
if(lpMessage) sprintf(lpMessage,"代理服务器:%s:%d通讯失败",pInfo->srvurl,pInfo->srvport); 
break; 
} 
} 
return(TRUE); 
}while(0); 
UMySocket::Close(); 
return(FALSE); 
}

//代理服务器连接 
#pragma pack(push,1) 
//sock4 req & ans 
typedef	struct	tagSock4Req 
{ 
char	vn; 
char	cd; 
WORD	port; 
DWORD	addr; 
char	other[1]; 
}SOCK4REQ,*PSOCK4REQ; 

typedef	struct	tagSock4Ans 
{ 
char	vn; 
char	cd; 
}SOCK4ANS,*PSOCK4ANS; 
//sock5 req & ans 
typedef	struct	tagSock5Req 
{ 
char	ver; 
char	lmethods; 
char	methods[255]; 
}SOCK5REQ,*PSOCK5REQ; 

typedef	struct	tagSock5Ans 
{ 
char	ver; 
char	method; 
}SOCK5ANS,*PSOCK5ANS; 
//sock5 check user 
typedef	struct	tagAuthReq 
{ 
char	ver; 
char	ulen; 
char	user[255]; 
char	plen; 
char	passwd[255]; 
}AUTHREQ,*PAUTHREQ; 

typedef	struct	tagAuthAns 
{ 
char	ver; 
char	status; 
}AUTHANS,*PAUTHANS; 

typedef	struct	tagSock5ReqEx 
{ 
char	ver; 
char	cmd; 
char	rsv; 
char	atyp; 
long	addr; 
WORD	port; 
}SOCK5REQEX,*PSOCK5REQEX; 

typedef	struct	tagSock5AnsEx 
{ 
char	ver; 
char	rep; 
char	rsv; 
char	atyp; 
char	other[1]; 
}SOCK5ANSEX,*PSOCK5ANSEX; 
#pragma pack(pop)

 

37.ping

38.telnet

 

39.close函数,fork

答:参考《UNP》卷1,第94页。close是引用计数-1,在没有到0的时候是不会关闭套接字的。fork调用会使父进程打开的socket引用计数+1,。所以一般多进程里面,父进程在fork子进程之后,父进程可以关闭accept套接字,子进程可以关闭listen套接字。这个时候两个套接字计数都从2减到1,所以不会关闭。所以父进程可以只做监听,子进程只做通信。

 

40.Linux终端调试命令

netstat -nalp|grep 8011 #查看8011端口的连接情况,观察TCP状态图
netstat -nalp|grep 8011|wc -l #查看8011端口的客户端连接数
ulimit -n 102400 #修改当前进程的最大文件数

tcpdump -i any 'tcp port 80'

lsof -i -Pn #lsof是list opened fd的单词缩写

netstat -anip

 

41.Windows cmd命令

netstat -ano|findstr "8011"#查看8011端口的连接情况,观察TCP状态图

-------

https://leetcode.com/

tcp/ip详解 第1卷,UNP,APUE,TCP/IP协议族

《编程珠玑第2版·修订版》

《编程珠玑(续)(修订版)》

《编程之美——微软技术面试心得》 
《剑指OFFER:名企面试官精讲典型编程题(第2版)》 

《程序员代码面试指南:IT名企算法与数据结构题目最优解》

《程序员面试宝典(第5版)》

 

 

  • 14
    点赞
  • 82
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值