效果演示在第二篇文章中


文章目录

  • 关于套接字和服务端的一些API
  • " 一个服务端面对应多个客户端 " 多线程思想
  • 网络编程的两个细节(超重要)
  • 服务端源码(超详细)

TCP/IP和Socket

对于网络编程,我们也称之TCP/IP,似乎其它网络协议已经不存在了。
对于TCP/IP,我们还知道TCP和UDP(使用UDP编程在后继会更新),前者可以保证数据的正确和可靠性,后者则允许数据丢失
最后,我们还知道,在建立连接前,必须知道对方的IP地址和端口号(什么是ip地址、端口号我就不介绍了)。


关于套接字和服务端的一些API

NioServer 多个客户端连接一个服务端会端口冲突吗_socket

  • SOCK_STREAM:流式套接字,提供可靠且有连接的传输(TCP)
  • Socket:创建套接字
  • bind:绑定地址(IP 和端口号)
  • listen:监听申请的连接
  • accept:等待连接
  • send/recv:发送数据 / 接收数据
  • close:释放套接字

上面这些API的顺序,就是服务端的程序编写顺序.


" 一个服务端对应多个客户端 " 多线程思想

因为TCP是面向连接的,所以一般的操作是没有办法完成一对多的

  1. 服务端要一直等待客户端的连接
  2. 每连接一个客户端,为客户端创建一个多线程(一直工作的多线程,收发数据)

大概思路如图所示:

NioServer 多个客户端连接一个服务端会端口冲突吗_套接字_02


网络编程的两个细节(超重要)

  • IP地址转换
    在我们生活中,通常用点分十进制来表示一个ip地址,但这不是一个真正的ip地址,计算机是不认识的
    我们需要将它进行转换,例如:
  • NioServer 多个客户端连接一个服务端会端口冲突吗_客户端_03


  • NioServer 多个客户端连接一个服务端会端口冲突吗_服务端_04

  • 字节转换
  • NioServer 多个客户端连接一个服务端会端口冲突吗_socket_05

主机一般对应小端字节,而网络则是大端字节

那什么是大端和小端呢?

假如同样一个数据,它们的表示方式可能不一样

例如下面图中小端是个位数6,大端是千位数6,管道则是网络:

NioServer 多个客户端连接一个服务端会端口冲突吗_客户端_06

小端转大端(主机转网络):htons、htonl(short和long)
大端转小端(网络转主机):ntohs、ntohl(short和long)


服务端源码(超详细)

主要实现:服务端接收多个客户端的数据
每行代码都加以注释,为了您理解的更快一点哟亲~

#pragma comment(lib, "ws2_32.lib")	// 网络编程需要的链接库(最新的版本号)

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>	// 2.2 版本

// 线程回调函数(接收客户端的信息)
DWORD CALLBACK ThreadProc(LPVOID lp)	// lp是接收到的最新的套接字
{
    SOCKET 	NewConnection = (SOCKET)lp;	// 转型
    char	DataBuffer[1024];		// 用于存储接收到的数据
    int 	Ret;				// 用于判断

    while(1)	// 一直要接收这个客户端的数据
    {
        // 接收数据并判断是否成功,该函数返回接收字符个数
        if((Ret = recv(NewConnection, DataBuffer, sizeof(DataBuffer), 0)) == 0)
        {
            // 返回错误数据
            printf("recv failed with error %d\n", WSAGetLastError()));
            closesocket(NewConnection);		// 释放套接字
            WSACleanup();			// 清空启动信息
            return 0;
        }

        printf("We successfully received %d bytes.\n", Ret);	// 打印接收了多少数据
        DataBuffer[Ret] = '\0';		// 接收的数据是没有 \0的
        printf("%s\n", DataBuffer);	// 打印数据
    }

    closesocket(NewConnection);		// 释放套接字	    
}

void main()
{
    WSADATA		wsaData;	// 一种用于初始启动信息的数据结构,加载winsocket.dll
    SOCKET		ListeningSocket;// 服务端的套接字
    SOCKET		NewConnection;	// 接受到的客户端的套接字
    SOCKADDR_IN		ServerAddr;	// 服务端地址
    SOCKADDR_IN		ClientAddr;	// 客户端地址
    int			ClientAddLen;	// 客户端地址长度
    int 		Port = 5150;	// 端口号(不能是太小的数,可以被其它程序所用)

    // 初始启动信息(加载版本号,模块),并判断是否成功
    if((Ret = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
    {
        printf("WSAStartup failed with error %d\n", Ret);
        return;
    }

    // 创建Socket, 使用 IPv4 进行通信(Windows只有AF_INET)、流式套接字、TCP协议
    if((ListeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) == INVALID_SOCKET)
    {
        printf("socket failed with error %d\n", WSAGetLastError());
        WSACleanup();	// 与 WSAStartup 配套使用
        return;
    }
    
    // 初始化地址信息
    ServerAddr.sin_family = AF_INET;	// 地址格式
    ServerAddr.sin_port = htons(Port);	// 地址端口号(字节转换成网络大端模式)
    ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);	// 接受本机内的所有ip地址(长整形,转化字节)
    
    // 绑定地址,并判断是否成功
    if((bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR))
    {
        printf("bind failed with error %d\n", WSAGetLastError());
        closesocket(ListeningSocket);	// 释放套接字
        WSACleanup();		
        return;
    }
    
    // 监听请求信号,最多有五个客户端(自己设置,不要太多),并判断是否成功
    if(listen(ListeningSocket, 5) == SOCKET_ERROR)
    {
    	printf("listen failed with error %d\n", WSAGetLastError());
    	closesocket(ListeningSocket);
    	WSACleanup();
    	return;
    }

    // 提示信息
    printf("We are waiting a connection on port %d.\n", Port);
    printf("Listen(正在监听)...\n");

    while(1)	// 一直等待客户端连接
    {
        ClientAddrLen = sizeof(SOCKADDR);	// 求出地址的长度
        // 接收请求(链接客户端),第二个参数是客户端的地址
        if((NewConnection = accept(ListeningSocket, (SOCKAddr*)&ClientAddr, &ClientAddrLen))
            == INVALID_SOCKET)
        {
            printf("accept failed with error %d\n", WSAGetLastError());
     	    closesocket(ListeningSocket);
     	    WSACleanup();
            return;
        }    
        
        // 链接客户端成功,打印客户端的ip地址和端口号,(ip地址转换和字节转换)
        printf("We successfully got a connection from %s:%d\n", inet_ntoa(ClientAddr.sin_addr), 
            ntohs(ClientAddr.sin_port));    

        printf("We are waiting to receive data\n");

        // ******************创建多线程******************
        CreateThread(NULL, 0, ThreadProc, (LPVOID)NewConnection, 0, NULL);
    }  

    printf("We are now going to close the client connection.\n");    
    closesocket(NewConnection);
    WSACleanup();
    system("pause");
}

客户端链接:

(二)TCPSocket 客户端编写 —— 一个服务端对应多个客户端(超详细)


作者:浪子花梦