网络程序之TCP、UDP篇(其一)

我记得最开始接触网络程序是在我读大二的时候,当时我做的是一个聊天的程序,也不知道服务器和客户端的概念,在网上就是一顿找啊,才到自己能看懂的答案,但是只能两个程序能聊天。造成这样的原因是程序是阻塞的,然而那时我并不知道什么是阻塞,所以程序就搁置到那了。这个问题一直到我大四在一家音视频公司实习的时候才解决了,也是我对网络程序了解更深了一步。结合我自己对网络编程的经验,写了关于网络程序的篇章,一共有三篇。之所以想写下来,是因为想着我在学网络程序初期走了很多不必要的弯路,如果有正在想学网络程序的同僚,希望这篇文章能让你少走弯路。

废话不多说,开始听我洗脑。哈哈

所谓网络程序,就是写的程序能通过网络进行通信(就是交互数据),所以要想程序间能进行通信,至少要运行两个实例(其中,一个实例为服务器,一个为客户端)。一般来说,服务器实例只要一个,客户端实例可以多个。服务器和客户端的实例可以是同一个程序(该程序既是服务器,又是客户端,一般用于局域网),也可以是不同的程序(服务器一个程序,客户端一个程序)。
在这里插入图片描述

如何建立TCP链接?(相关代码都使用C++进行演示)

首先,个人建议先了解一下TCP协议,以及TCP链接建立的三次握手、断开的四次挥手。期间服务器和客户端状态变化建议了解比较好,这里不做讲解,感兴趣的可以去看看。

如果你不想了解那些原理,也没关系,并不影响编程。

服务端(只要记住六步):

1)创建套接字(socket)

2)绑定地址(bind)

3)监听(listen)

4)轮询等待客户端的接入(select | accept)

5)接收,发送消息(recv | send)

6)关闭(close)

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>
#pragma comment(lib, “ws2_32.lib”)

int main()
{
//检测版本号
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
::WSAStartup(wVersionRequested, &wsaData);

//1)创建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP0);

//2)绑定地址
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = ::htons(8888);
addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
::bind(s, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

//3)监听
::listen(s, 5);

//4)轮询等待客户端的接入
int iLen = sizeof(SOCKADDR);
while (true) {
    SOCKADDR_IN clientAddr;
    SOCKET client = ::accept(s, (SOCKADDR *)&clientAddr, &iLen);
    
    //5)发送消息
    ::send(client, "sever", 6, 0);
}

//6)关闭
closesocket(s);
return 0;

}

客户端(只要记住四步)

1)创建套接字(scoket)

2)连接服务器(connect)

3)接收,发送消息(recv | send)

4)关闭(close)

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
    //检测版本号
    WORD wVersionRequested;
    WSADATA wsaData;
    wVersionRequested = MAKEWORD(1, 1);
    ::WSAStartup(wVersionRequested, &wsaData);

    //1)创建套接字
    SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //2)连接
    SOCKADDR_IN addrServer;
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = ::htons(8888);
    addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
    ::connect(s, (sockaddr*)&addrServer, sizeof(addrServer));

    //3)接收消息
    char szBuf[1024] = { 0 };
    int ret = recv(s, szBuf, 1024, 0);
    szBuf[ret] = '\0';
    printf("%s\n", szBuf);

    //4)关闭
    closesocket(s);
    return 0;
}

上述代码是基于WinSocket编写的,visual studio编辑可直接运行。为了便于理解,很多异常情况没有判断。 看了上面客户端,和服务器的代码是不是觉得很容易?但是上诉程序只能保证一个客户端进行正常交互。为什么呢?原因就是服务器中的accept函数是阻塞函数,也就是当有新的客户端连接到服务器时,accept才会返回,否则一直处于等待状态。那么如何解决呢?这里可能很多同僚也知道,使用select模式轮询,或者epoll模式(Linux独有)。这里给大家科普一下,早期还没有select模式和epoll模式的时候,是如何解决阻塞的呢?是用线程处理的,当时最早的魔兽世界服务器据说是用线程处理的阻塞问题。因为accept,recv,send三个函数都是阻塞函数,所以服务端至少创建2倍的客户端+1的线程数量。这是在没有select和epoll模式的做法。但是线程多了,不仅消耗操作系统的资源,而且会让程序变得很复杂,不是很好维护。下面给出select模式下的,服务端和客户端代码。

服务端:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")


bool IsCanWrite(SOCKET s)
{
    fd_set writefds;
    FD_ZERO(&writefds);
    FD_SET(s, &writefds);

    timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;

    int ret = select(FD_SETSIZE, NULL, &writefds, NULL, &timeout);
    if (ret > 0 && FD_ISSET(s, &writefds)) {
        return true;
    }

    return false;
}

int main()
{
    //检测版本号
    WORD wVersionRequested;
    WSADATA wsaData;
    wVersionRequested = MAKEWORD(1, 1);
    ::WSAStartup(wVersionRequested, &wsaData);

    //1)创建套接字
    SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //2)绑定地址
    SOCKADDR_IN addrServer;
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = ::htons(8888);
    addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
    ::bind(s, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

    //3)监听
    ::listen(s, 5);

    //4)轮询等待客户端的接入
    SOCKET sMax = s;
    int iLen = sizeof(SOCKADDR);
    while (true) {
        //判断是否有新的连接
        fd_set fdRead;
        FD_ZERO(&fdRead);
        FD_SET(s, &fdRead);

        int ret = -1;
        timeval t_out = { 0, 0 };
        ret = select(sMax + 1, &fdRead, NULL, NULL, &t_out);
        if (ret > 0) {
            if (FD_ISSET(s, &fdRead)) {
                SOCKADDR_IN clientAddr;
                SOCKET client = ::accept(s, (SOCKADDR *)&clientAddr, &iLen);

                //5)发送消息
                //判断套接字是否可写
                if (IsCanWrite(client)) {
                    ::send(client, "sever", 6, 0);
                }
            }
        }

        //防止独占CPU
        Sleep(10);
    }

    //6)关闭
    closesocket(s);
    return 0;
}

客户端:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

bool IsCanRead(SOCKET s)
{
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(s, &readfds);

    timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;

    int ret = select(FD_SETSIZE, &readfds, NULL, NULL, &timeout);
    if (ret > 0 && FD_ISSET(s, &readfds)) {
        return true;
    }

    return false;
}

int main()
{
    //检测版本号
    WORD wVersionRequested;
    WSADATA wsaData;
    wVersionRequested = MAKEWORD(1, 1);
    ::WSAStartup(wVersionRequested, &wsaData);

    //1)创建套接字
    SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //2)连接
    SOCKADDR_IN addrServer;
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = ::htons(8888);
    addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
    ::connect(s, (sockaddr*)&addrServer, sizeof(addrServer));

    //3)接收消息
    while (true) {
        //判断套接字是否可读
        if (IsCanRead(s)) {
            char szBuf[1024] = { 0 };
            int ret = recv(s, szBuf, 1024, 0);
            szBuf[ret] = '\0';
            printf("%s\n", szBuf);
        }

        //防止独占CPU
        Sleep(10);
    }

    //4)关闭
    closesocket(s);
    return 0;
}

上述就是阻塞模式和非阻塞模式的代码,这里非阻塞模式只给了select模型,至于epoll模型,可以自行科普(epoll和select模型是有区别,根据不同的情况选择不同的模式,这里不做说明)。

如何建立UDP链接?(相关代码都使用C++进行演示)

还是个人建议先了解一下UDP协议,相对于TCP协议比较而言,UDP协议简单的多。

如果你不想了解那些原理,也没关系,并不影响编程。

服务端(只要记住四步):

1)创建套接字(socket)

2)绑定地址(bind)

3)接收,发送消息(recv | send)

4)关闭(close)

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
    //检测版本号
    WORD wVersionRequested;
    WSADATA wsaData;
    wVersionRequested = MAKEWORD(1, 1);
    ::WSAStartup(wVersionRequested, &wsaData);

    //1)创建套接字
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    //2)绑定地址
    SOCKADDR_IN addrServer;
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = ::htons(9999);
    addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
    ::bind(s, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

    //3)接收消息
    while (true) {
        sockaddr_in addr;
        int addr_len = sizeof(SOCKADDR);
        char szBuf[1024] = { 0 };
        int ret = recvfrom(s, szBuf, 1024, 0, (sockaddr*)&addr, &addr_len);
        if (ret > 0) {
            szBuf[ret] = '\0';
            printf("%s\n", szBuf);
        }

        //防止CPU空转
        Sleep(10);
    }

    //4)关闭
    closesocket(s);
    return 0;
}

客户端(只要记住三步):

1)创建套接字(socket)

2)接收,发送消息(recv | send)

3)关闭(close)

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
    //检测版本号
    WORD wVersionRequested;
    WSADATA wsaData;
    wVersionRequested = MAKEWORD(1, 1);
    ::WSAStartup(wVersionRequested, &wsaData);

    //1)创建套接字
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);


    //2)发送消息
    SOCKADDR_IN addrServer;
    int addr_len = sizeof(SOCKADDR);
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = ::htons(9999);
    addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
    sendto(s, "client", 6, 0, (sockaddr*)&addrServer, addr_len);

    //3)关闭
    closesocket(s);
    return 0;
}

上述就是UDP连接的相关代码,为了便于理解,有些异常情况没有判断,在实战项目中,注意点就行。下面说一下TCP和UDP链接的特点,网同僚在以后开发中自己进行选取。

TCP链接:需要建立连接,稳定(超时重传机制),数据不容易丢失,流式数据;

UDP链接:无需建立链接,不稳定,数据可能会丢,报文数据

套接字属性?

其实套接字本身是还有很多属性的,如果仅仅以为掌握上述的select代码就算掌握的话,那你可就太天真了。上述本人给的select模式代码,默认是非阻塞的;其实也可以把select设置成超时阻塞模式,这就需要知道套接字属性字段了。下面我例举部分属性字段,主要目的是让知道套接字有属性就行,具体哪些可以网上查阅。

//下面是winsocket的属性,主要是命名不一样,Linux下也有,可能是别的命名方式
FIONBIO //阻塞模式
SO_LINGER //linger算法(节约流量,不知道的网上科普)
SO_SNDBUF //协议栈(这个概念不是很清楚的,先放一放,或者去科普,因为随着你写的越多,你就慢慢明白了)的发送缓冲区大小
SO_RCVBUF //协议栈的接收缓冲区大小
SO_SNDTIMEO //发送超时
SO_RCVTIMEO //接收超时
SO_REUSEADDR //重用地址
TCP_NODELAY //延时发送(如果开启,就可能会产生黏包)
//等等,还有很多字段,但是常用(本人常用)的就这么几个

到这里,关于套接字,TCP和UDP操作,基本差不多了,好像也就只这些。所以,不用把网络编程想的太困难。最后,附上本人套接字的封装代码,有需要的自行下载。本人只实现了C/C++语言(Windows/Linux)和PHP语言两个版本,其实都差不多,懂了原理各种语言都一样,都是API的名字不一样罢了。好吧,不说了,祝早日成功。

链接:https://pan.baidu.com/s/1IbZBsQxR2yUSs06aK5AmXw
提取码:jzt1

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值