效果演示在第二篇文章中
文章目录
- 关于套接字和服务端的一些API
- " 一个服务端面对应多个客户端 " 多线程思想
- 网络编程的两个细节(超重要)
- 服务端源码(超详细)
TCP/IP和Socket
对于网络编程,我们也称之TCP/IP,似乎其它网络协议已经不存在了。
对于TCP/IP,我们还知道TCP和UDP(使用UDP编程在后继会更新),前者可以保证数据的正确和可靠性,后者则允许数据丢失。
最后,我们还知道,在建立连接前,必须知道对方的IP地址和端口号
(什么是ip地址、端口号我就不介绍了)。
关于套接字和服务端的一些API
- SOCK_STREAM:流式套接字,提供可靠且有连接的传输(TCP)
- Socket:创建套接字
- bind:绑定地址(IP 和端口号)
- listen:监听申请的连接
- accept:等待连接
- send/recv:发送数据 / 接收数据
- close:释放套接字
上面这些API的顺序,就是服务端的程序编写顺序.
" 一个服务端对应多个客户端 " 多线程思想
因为TCP是面向连接的,所以一般的操作是没有办法完成一对多的
- 服务端要一直等待客户端的连接
- 每连接一个客户端,为客户端创建一个多线程(一直工作的多线程,收发数据)
大概思路如图所示:
网络编程的两个细节(超重要)
- IP地址转换
在我们生活中,通常用点分十进制来表示一个ip地址,但这不是一个真正的ip地址,计算机是不认识的
我们需要将它进行转换,例如: - 字节转换
主机一般对应小端字节,而网络则是大端字节
那什么是大端和小端呢?
假如同样一个数据,它们的表示方式可能不一样
例如下面图中小端是个位数6,大端是千位数6,管道则是网络:
小端转大端(主机转网络):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 客户端编写 —— 一个服务端对应多个客户端(超详细)
作者:浪子花梦