知识来源一:
使用Dev-C++实现简单的客户端和服务器-CSDN博客
此先生的博客使用的是win32 SDK来创建多线程,然后鄙人对这个版本的多线程细节不明。于是又重新用C语言的线程替代win32API,以此继续学习服务器代码。
知识来源二:DevC++ 多线程创建与删除与exe文件脱离DevC++运行中发现dll文件和exe文件的关系-CSDN博客
这是C语言多线程简单样例。
然后在这两个基础上优化知识来源一的服务端代码,过程中查询函数,然后复制博客链接和截取解释,产生了知识来源三如下,作为函数忘了可以怎么开发了,就翻回去参考的,集成小文案。
知识来源三:
DevC++ socket嵌套字实现局域网客户端服务端函数详解注释-CSDN博客
优化功能说明:
客户端命令: log_out : 切断客户端在服务器的联系
消息反馈功能:客户端输入消息后,服务端发送反馈消息,说明可以服务端可以直接利用已经建立的链接传输消息,不用再另外建一新联系。
增加过程提示:函数执行过程中printf运行阶段,是当时改线程创建忘新建线程的bug加的测试点。
效果如图
零起点的顺序是,先看知识来源二的文章,玩玩里面的代码demo,键盘命令创建取消线程。然后看本节文章,对着知识点三,把本节文章的单线程服务器注释取消掉,删掉多线程部分。理解一个服务器一个客户端的联系的建立,然后再看看多线程代码,读读注释,用自己的话再说说,然后自己独立写写多线程服务器代码,写20min要是写不动了就仔细描述自己怎么写不出来的,是不会无中生有,连猜也猜不出来通信需要什么凭证,还是说是函数参数忘了默认值。然后再看看参考代码,理解自己是通过怎样的观察,在限定的时间里发现自己重复中却不能重复出新的思路。避免死磕,发现规模重复不能产生新意就回归,在现有的参考标准里的继续学习,临摹思路与代码。然后再试试能不能整活加点料,实现新功能。
完整代码如下,如果服务器代码编译的exe文件点击运行时,提示缺少libwinpthread-1.dll文件,有bug原理与解决方案,详情刚才的知识来源二:DevC++ 多线程创建与删除与exe文件脱离DevC++运行中发现dll文件和exe文件的关系-CSDN博客
服务端,有小的注释。
#include <stdio.h>
#include <winsock2.h>
#include<pthread.h>
#include<string.h>
#include<conio.h>
#pragma comment(lib,"ws2_32.lib")
//这两个注释块是win32API函数写的的多线程函数,用于对照c语言多线程
//typedef struct ThreadNode
//{
// int index;
// HANDLE ThreadId;
// SOCKET Client;
// struct ThreadNode * next;
// ThreadNode()
// {
// index = 0;
// this->next = NULL;
// }
//}hThread;
typedef struct ThreadNode {
int index;
pthread_t* Thread;
SOCKET Client;
struct ThreadNode * next;
ThreadNode() {
index = 0;
this->next = NULL;
}
} hThread;
hThread *clientHeadNote, *clientEndNote;
hThread * addClient() {
hThread * ClientNote = new hThread();
return ClientNote;
}
//DWORD WINAPI ThreadClient(LPVOID param)
//{
// if(clientEndNote == NULL)
// {
// printf("empty Link\n");
// return 0;
// }
// char revData[255];
// SOCKET sClient = clientEndNote->Client;
// while(1)
// {
// //接收数据
// int ret = recv(sClient, revData, 255, 0);
// if(ret > 0)
// {
// revData[ret] = 0x00;
// printf(revData);
// puts(0);
// }
// }
// closesocket(sClient);
//}
//每个建立链接的客户端分配一个函数,每个线程运行一个这样的函数
void* ThreadClient(void*) {
if (clientEndNote == NULL) {
printf("empty Link\n");
return 0;
}
char revData[255];
SOCKET sClient = clientEndNote->Client;
printf("client logged in \n");
while (1) {
//接收数据
int ret = recv(sClient, revData, 255, 0);
if (ret > 0) {
revData[ret] = 0x00;
printf(revData);
// puts(0);
printf("\n");
// 新功能:加入客户端控制签退
if (strcmp("log_out", revData) == 0) {
char a[255];
strcpy(a, "已注销\n");
send(sClient, a, 255, 0);
// 反馈:向客户端sClienr发送“已注销”消息,发送长度为255个字节,0是指不阻塞。
// printf("签退成功\n");
closesocket(sClient);
printf("签退成功\n");
} else {
char a[255];
strcpy(a, "服务器已收到消息\n");
send(sClient, a, 255, 0);
}
}
}
closesocket(sClient);
}
int main(int argc, char* argv[]) {
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (WSAStartup(sockVersion, &wsaData) != 0) {
return 0;
}
//创建套接字
SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (slisten == INVALID_SOCKET) {
printf("socket error !");
return 0;
}
//绑定IP和端口
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8888);
// 端口和客户端端口要相同,否则链接建立不上
// sin.sin_port = htons(8880);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 这个是any指的是不限制访问IP
// sin.sin_addr.S_un.S_addr =htonl(INADDR_ANY);
// char loa[16] = "192.168.15.189";
// sin.sin_addr.S_un.S_addr = inet_addr(loa);
// 把sin强制类型转换为sockadd,sin原来是sockaddr_in类型
// 初始化slisten,加载上面代码设置的属性。
if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) {
printf("bind error !");
}
//开始监听
if (listen(slisten, 5) == SOCKET_ERROR) {
printf("listen error !");
return 0;
}
// 以上可以认为是建立服务器联系的固定格式
//下面注释部分是单线程的服务器,就是只能接受一个客户端的服务器
//循环接收数据
// SOCKET sClient;
// sockaddr_in remoteAddr;
// int nAddrlen = sizeof(remoteAddr);
// char revData[255];
接受消息拉到外部,建立联系就一直接受
// printf("等待连接...\n");
// sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
// if (sClient == INVALID_SOCKET) {
// printf("accept error !");
continue;
// }
// printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));
//
//
// while (true) {
// //接收数据
// int ret = recv(sClient, revData, 255, 0);
ret是数据长度 sclient是数据来源 revdata是数据,255是长度上限,0是不阻塞
// if (ret > 0) {
// revData[ret] = 0x00;
// printf(revData);
// printf("\n");
// }
// //发送数据
// char a[100]="服务器已收到消息\n";
scanf("%s", a);
// send(sClient, a, strlen(a), 0);
// }
//
// closesocket(slisten);
// WSACleanup();
// return 0;
//这里是多线程服务器,接受多个客户端,可以对比单线程看看多了什么,哪些函数的位置调整了
sockaddr_in remoteAddr;
int nAddrlen = sizeof(remoteAddr);
while (true) {
printf("等待连接...\n");
SOCKET sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
if (sClient == INVALID_SOCKET) {
printf("accept error !");
continue;
}
printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));
if (clientHeadNote == NULL) {
clientHeadNote = addClient();
// printf("node creat\n");
clientEndNote = clientHeadNote;
}
// clientEndNote->Client = sClient;
// clientEndNote->ThreadId = CreateThread(NULL, 0, ThreadClient, NULL, 0, NULL);
// 必须要先创建给end节点,然后再创建线程,因为线程的参数通过全局变量传入,而不是通过线程生成函数传入
// 原来线程参数除了 pthread_create()还能通过全局变量传参数
hThread* clientnode = (hThread*)malloc(sizeof(hThread));
clientnode->Client = sClient;
clientnode->next = NULL;
clientHeadNote->next = clientnode;
clientEndNote = clientnode;
printf("endnode is created\n");
clientnode->Thread = (pthread_t*)malloc(sizeof(pthread_t));
clientnode->index = pthread_create(clientnode->Thread, NULL, ThreadClient, NULL);
printf("线程创建成功\n");
}
printf("server is closing\n");
hThread * p;
while (clientHeadNote) { //释放占用的内存空间
// CloseHandle(clientHeadNote->ThreadId);
p = clientHeadNote;
clientHeadNote = clientHeadNote->next;
delete p;
}
closesocket(slisten);
WSACleanup();
return 0;
}
客户端,但是注释更零散
#include <WINSOCK2.H>
#include <STDIO.H>
#include<pthread.h>
#pragma comment(lib,"ws2_32.lib")
//格式和服务端一样
int main(int argc, char* argv[]) {
WORD sockVersion = MAKEWORD(2, 2);
WSADATA data;
if (WSAStartup(sockVersion, &data) != 0) {
return 0;
}
SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sclient == INVALID_SOCKET) {
printf("无效的 socket !");
return 0;
}
sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
//表示使用IPv4地址协议
// https://blog.csdn.net/u012736362/article/details/130392547
serAddr.sin_port = htons(8888);
// puts("请输入对方的IP地址");
char loa[16] = "127.0.0.1";
// 就是当服务器的电脑的IP , win建+r 输入cmd,打开命令行,输入再直接 ipconfig/all可查看IP。
// IP是读取目标机器的IP,由于一台电脑当服务器和客户端,127.0.0.1是自回路,自己发送给自己
serAddr.sin_addr.S_un.S_addr = inet_addr(loa);
// IP赋值
//链接发消息
if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR) {
//主动连接服务器
printf("连接错误 !");
closesocket(sclient);
return 0;
}
puts("连接成功!!");
puts("你现在可以向服务器发送信息:");
while (1) {
char sendData[100];
// gets(sendData);
scanf("%s",sendData);
send(sclient, sendData, strlen(sendData), 0);
// send(sclient, sendData, 255, 0);
// 匹配长度 255个字节
// Sleep(1500);
// if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR) {
// //主动连接服务器
// printf("连接错误 !");
// closesocket(sclient);
// return 0;
// }
// puts("连接成功!!");
// puts("你现在可以向服务器发送信息:");
char recData[255];
int ret = recv(sclient, recData, 255, 0);
// 接收服务端消息
if (ret > 0) {
recData[ret] = 0x00;
printf(recData);
}
}
// char recData[255];
// int ret = recv(sclient, recData, 255, 0);
// if(ret > 0)
// {
// recData[ret] = 0x00;
// printf(recData);
// }
closesocket(sclient);
WSACleanup();
return 0;
}