TCP/UDP网络通信

OSI参考模型

  • 简介
    • 物理层:定义物理接口
    • 数据链路层:mac寻址
    • 网络层:IP寻址
    • 传输层:数据分段、建立连接
    • 会话层:管理会话
    • 表示层:定义数据格式、加密
    • 应用层:为应用程序提供服务

TCP三次握手与四次挥手

TCP三次握手

  • 第一次握手:建立连接时,客户端发送syn包(seq=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)
  • 第二次握手:第二次握手:服务器收到syn包,必须确认客户端的SYN(ack=j+1),同时自己也发送一个SYN包(seq=k),即SYN+ACK包,此时服务器进入SYN_RECV状态
  • 第三次握手:第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手

注:第三次握手是为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。譬如发起请求遇到类似这样的情况:客户端发出去的第一个连接请求由于某些原因在网络节点中滞留了导致延迟,直到连接释放的某个时间点才到达服务端,这是一个早已失效的报文,但是此时服务端仍然认为这是客户端的建立连接请求第一次握手,于是服务端回应了客户端,第二次握手

TCP四次挥手

  • 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送
  • 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1
  • 服务器B关闭与客户端A的连接,发送一个FIN给客户端A
  • 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1

Windows UDP程序实例

server.cpp

#include<winsock2.h>
#include<stdio.h>
#include<time.h>
#include<string.h>
#include<iostream>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
#define SERVER_PORT 8081 
int main()
{
    WSADATA wsa;
    WSAStartup(WINSOCK_VERSION,&wsa);//初始化WS2_32.DLL
    SOCKET serversoc;
    SOCKET clientsoc;
    SOCKADDR_IN serveraddr;
    SOCKADDR_IN clientaddr;

    int client_len = sizeof(clientaddr);
    int server_len = sizeof(serveraddr);
    char *Send_data;
    int Send_len;
    char Recv_buf[64];
    char Send_buf[64];
    int result;
    time_t nowtime;
    //命名协议,IP,端口
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SERVER_PORT);
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    //创建socket
    socket = socket(AF_INET,SOCK_DGRAM,0);
    //绑定socket
    socket=bind(serversoc, (SOCKADDR *)&serveraddr, server_len);
    (result==SOCKET_ERROR)
    {
        ("bind socket failed!\n");
        (serversoc);
        -1;
    }
    printf("Server is running.....\n");
    clientsoc = socket(AF_INET,SOCK_DGRAM,0);
    while(1)
        {   
            //接收数据
            result = recvfrom(serversoc,Recv_buf,64,0,(SOCKADDR *)&clientaddr,&client_len);

            if(result >= 0)
            {
                Recv_buf[result]= 0;
                printf("recieve message:  %s \n",Recv_buf);
                            //发送数据
                            //nowtime=time(0);
                            //Send_data=ctime(&nowtime);
                printf("please input your message: ");
                cin >> Send_buf;
                Send_len=strlen(Send_buf);
                result = sendto(clientsoc,Send_buf,Send_len,0,(SOCKADDR *)&clientaddr,sizeof(clientaddr));
                printf("message has send:  %s \n", Send_buf);
            }
    }
    closesocket(serversoc);
    WSACleanup();
    return 0;
}

client.cpp

#include<winsock2.h>
#include<stdio.h>
#include<string.h>
#include<iostream>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
# define BUFFER_SIZE 1024 //缓冲区大小
# define SERVER_PORT 8081
# define SERVER_IP "127.0.0.1"
int main()
{
    SOCKET sock_Client; //客户端用于通信的Socket
    WSADATA WSAData;//这个结构被用来存储被WSAStartup函数调用后返回的 Windows Sockets数据。它包含Winsock.dll执行的数据。

    char receBuf[BUFFER_SIZE]; //发送数据的缓冲区
    char sendBuf[BUFFER_SIZE]; //接受数据的缓冲区

    //向操作系统说明,我们要用哪个库文件,让该库文件与当前的应用程序绑定,从而就可以调用该版本的socket的各种函数
    //进行相应的socket库绑定
    if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
    {
        printf("init failed!");
        return -1;
    } 
    
    //初始化              (IPV4/v6 ,流式/数据报,UDP/TCP)
    sock_Client = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//创建客户端用于通信的Socket
    SOCKADDR_IN addr_server; //服务器的地址数据结构
    addr_server.sin_family = AF_INET;
    addr_server.sin_port = htons(SERVER_PORT);//端口号
    addr_server.sin_addr.S_un.S_addr = inet_addr(SERVER_IP); //server IP地址
    SOCKADDR_IN sock;
    int len = sizeof(sock);
    while (true)
    {
        cout << "please input your message:";
        cin >> sendBuf;
        sendto(sock_Client, sendBuf, strlen(sendBuf), 0, (SOCKADDR*)&addr_server, sizeof(SOCKADDR));
        printf("now waiting for reply: \n");
        while(1)
        {
            //接收
            //int last = recv(sock_Client, receBuf, strlen(receBuf), 0); // (调用recv和recvfrom都可以)
            int last = recvfrom(sock_Client, receBuf, 10240, 0, (SOCKADDR*)&sock, &len);
            if (last>0)
            {
                receBuf[last] = '\0'; //给字符数组加一个'\0',表示结束了。不然输出有乱码
                printf("recieve from server:%s\n", receBuf);
                break;
            }
        }
    }
    closesocket(sock_Client);
    WSACleanup();
    return 0;
}

Socket的阻塞模式和非阻塞模式

阻塞模式

Windows套接字在阻塞和非阻塞两种模式下执行I/O操作。在阻塞模式下,在I/O操作完成前,执行的操作函数一直等候而不会立即返回,该函数所在的线程会阻塞在这里。相反,在非阻塞模式下,套接字函数会立即返回,而不管I/O是否完成,该函数所在的线程会继续运行。

并不是所有Windows Sockets API以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会立即返回。将可能阻塞套接字的Windows Sockets API调用分为以下四种:

1.输入操作
recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。如果此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。

2.输出操作
send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。

3.接受连接
accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。

4.外出连接
connect()和WSAConnect()函数。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少到服务器的一次往返时间。

阻塞模式特点:
优点:使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。
缺点:阻塞模式套接字的不足表现为,在大量建立好的套接字线程之间进行通信时比较困难。当使用“生产者-消费者”模型开发网络程序时,为每个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当希望同时处理大量套接字时,将无从下手,其扩展性很差。

非阻塞模式

把套接字设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字多次调用recv()函数的过程。前三次调用recv()函数时,内核数据还没有准备好。因此,该函数立即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。

当使用socket()函数和WSASocket()函数创建套接字时,默认都是阻塞的。在创建套接字之后,通过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl().
套接字设置为非阻塞模式后,在调用Windows Sockets API函数时,调用函数会立即返回。大多数情况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK错误代码。说明请求的操作在调用期间内没有时间完成。通常,应用程序需要重复调用该函数,直到获得成功返回代码。需要说明的是并非所有的Windows Sockets API在非阻塞模式下调用,都会返回WSAEWOULDBLOCK错误。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代码。当然,在调用WSAStartup()函数时更不会返回该错误代码,因为该函数是应用程序第一调用的函数,当然不会返回这样的错误代码。

要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数之外,还可以使用WSAAsyncselect()和WSAEventselect()函数。当调用该函数时,套接字会自动地设置为非阻塞方式。

由于使用非阻塞套接字在调用函数时,会经常返回WSAEWOULDBLOCK错误。所以在任何时候,都应仔细检查返回代码并作好对“失败”的准备。应用程序连续不断地调用这个函数,直到它返回成功指示为止。上面的程序清单中,在While循环体内不断地调用recv()函数,以读入1024个字节的数据。这种做法很浪费系统资源。

要完成这样的操作,有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。同样,这种方法也不好。因为该做法对系统造成的开销是很大的,并且应用程序至少要调用recv()函数两次,才能实际地读入数据。较好的做法是,使用套接字的“I/O模型”来判断非阻塞套接字是否可读可写。

非阻塞模式特点:
优点:非阻塞套接字在控制建立的多个连接,在数据的收发量不均,时间不定时,明显具有优势。这种套接字在使用上存在一定难度,但只要排除了这些困难,它在功能上还是非常强大的。通常情况下,可考虑使用套接字的“I/O模型”,它有助于应用程序通过异步方式,同时对一个或多个套接字的通信加以管理。
缺点:非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,需要编写更多的代码,以便在每个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。因此,非阻塞套接字便显得有些难于使用。

发表评论

您的电子邮箱地址不会被公开。