首先,基于我以后打算从事C语言开发,目前我掌握的C语言知识匮乏。目前打算通过一个个的小项目,逐渐完善我的C语言开发方面相关知识,这篇文章便是一个开始。写这篇文章的动力是为了检验我的一天的学习成果。如有热心的朋友看出文章中的不足,恳请批评指正。
Windows下用C实现一个简单的聊天窗口,便是我的第一个项目。
在这个项目里,我主要可以掌握C语言的多线程实现思路,以及加深对于TCP连接,客户端/服务器数据交换的相关理解。
该项目也是在前人的思路上进行操作的,此处附上大佬链接
(C语言实现聊天室(windows版本) - 掘金 (juejin.cn))。
我在对他的代码进行CV编程之后,出现了一些错误,主要是新版的vs不支持一些老版的函数不适用,需要修改,比如
sprinntf_s(...)字符串拼接函数
int sprintf_s(
char *buffer, //Storage location for output
size_t sizeOfBuffer, //Maximum number of characters to store.
const char *format, //Format-control string
...
);
inet_pton(...) and inet_ntop(...)这两个是新型网络地址转化函数。((1条消息) inet_pton()和inet_ntop()函数详解_QvQ是惊喜不是哭泣的博客-CSDN博客)
#include <arpe/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr); //将点分十进制的ip地址转化为用于网络传输的数值格式
返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //将数值格式转化为点分十进制的ip地址格式
返回值:若成功则为指向结构的指针,若出错则为NULL
将上面那位掘金大佬的代码,如上的两个函数修改。即可成功运行。
代码中通过
- g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
- WaitForSingleObject(g_hEvent, INFINITE);
- SetEvent(g_hEvent);//设置授信,释放信号量,其他客户端可以连接。
- WaitForMultipleObjects(clnt_cnt, hThread, TRUE, INFINITE);//等待所有的客户端线程信号
这个内核对象来控制多线程的并发,下面我们讲讲内核对象。
定义:内核对象通过API来创建,每个内核对象是一个数据结构,它对应一块内存, 由操作系统内核分配,并且只能由操作系统内核访问。在此数据结构中少数成员如安全描述符和使用计数是所有对应都有的,但其他大多数成员都是不用类型的对象特有的。内核对象的数据结构只能由操作系统提供的API访问,应用程序在内存中不能访问。调用创建内核对象的函数后,该函数会返回一个句柄,它标识了所创建的对象。它可以由进程的任何线程使用。
通知状态(受信状态) 未通知状态(非受信状态)
当线程正在运行时,线程内核对象处于未通知状态。当线程停止运行时,就处于已通知状态。可以通过等待线程来检查线程是否仍然运行。我的理解,受信状态表示线程未处理事务,可以处理新接收到的连接请求。在这个多人聊天室的程序中,即可这样表述,“若在同一时间有多个客户端请求TCP连接,这时候,监听线程会将接收到的所有的请求放在缓存数组中,主线程开始处理TCP请求时,就将内核对象设置为未受信状态,然后线程开始处理TCP请求。处理完成,将主线程标记为通知状态,即等待接收信号的状态。
如下是聊天窗口服务器端代码(server.c)
#include <winsock2.h> // 为了使用Winsock API函数
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define MAX_CLNT 256
#define BUF_SIZE 100
#define INET_ADDRSTRLEN 34
unsigned short port = 80;
// 告诉连接器与WS2_32库连接
#pragma comment(lib,"WS2_32.lib")
typedef struct sockaddr_in sockaddr_in;
void error_handling(const char* msg); /*错误处理函数*/
DWORD WINAPI ThreadProc(LPVOID lpParam); /*线程执行函数*/
void send_msg(char* msg, int len); /*消息发送函数*/
HANDLE g_hEvent; /*事件内核对象*/
int clnt_cnt = 0; //统计套接字
int clnt_socks[MAX_CLNT]; //管理套接字
HANDLE hThread[MAX_CLNT]; //管理线程
int main()
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
WSAStartup(sockVersion, &wsaData); //请求了一个2.2版本的socket
// 创建一个自动重置的(auto-reset events),受信的(signaled)事件内核对象.管理线程之间的通信和同步。
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
// 创建套接字,AF_INET表示ipv4地址簇,SOCK_STREAM表示基于TCP的流式套接字,IPPROTO表示TCP协议。
SOCKET serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (serv_sock == INVALID_SOCKET)
error_handling("Failed socket()");
// 填充sockaddr_in结构
sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port); //80端口
sin.sin_addr.S_un.S_addr = INADDR_ANY; //本地地址 ,bind to any local address
//sin.sin_addr.S_un.S_addr = inet_pton("127.0.0.1");
// 绑定这个套节字到一个本地地址
if (bind(serv_sock, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
error_handling("Failed bind()");
// 进入监听模式
if (listen(serv_sock, 256) == SOCKET_ERROR) //最大连接数为2
error_handling("Failed listen()");
printf("Start listen:%d\n",port);
// 循环接受客户的连接请求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
DWORD dwThreadId; /*线程ID*/
SOCKET clnt_sock; /*用来存接收到客户端信号后新建的套接字*/
//char szText[] = "hello!\n";
while (TRUE)
{
printf("等待新连接\n");
// 接受一个新连接
clnt_sock = accept(serv_sock, (SOCKADDR*)&remoteAddr, &nAddrLen);
if (clnt_sock == INVALID_SOCKET)
{
printf("Failed accept()");
continue;
}
/*等待内核事件对象状态受信*/
WaitForSingleObject(g_hEvent, INFINITE); //表示等待 g_hEvent 事件对象的信号,等待时间设定为无限。也就是说,该函数会一直阻塞当前线程,直到 g_hEvent 收到信号或发生错误。
hThread[clnt_cnt] = CreateThread(
NULL, // 默认安全属性
NULL, // 默认堆栈大小
ThreadProc, // 线程入口地址(执行线程的函数)
(void*)&clnt_sock, // 传给函数的参数,新的生成的套接字
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
clnt_socks[clnt_cnt++] = clnt_sock;
SetEvent(g_hEvent); /*设置受信*/
char strip[INET_ADDRSTRLEN];
char* ptr = inet_ntop(AF_INET, &remoteAddr.sin_addr, strip, sizeof(strip));
printf(" 接受到一个连接:%s 执行线程ID:%d\r\n", strip, dwThreadId);
}
//等待所有客户端对象的信号。
WaitForMultipleObjects(clnt_cnt, hThread, TRUE, INFINITE);
for (int i = 0; i < clnt_cnt; i++)
{
CloseHandle(hThread[i]);
}
// 关闭监听套节字
closesocket(serv_sock);
// 释放WS2_32库
WSACleanup();
return 0;
}
void error_handling(const char* msg)
{
printf("%s\n", msg);
WSACleanup();
exit(1);
}
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
int clnt_sock = *((int*)lpParam);
int str_len = 0, i;
char msg[BUF_SIZE];
while ((str_len = recv(clnt_sock, msg, sizeof(msg), 0)) != -1)
{
send_msg(msg, str_len);
printf("群发送成功\n");
}
printf("客户端退出:%d\n", GetCurrentThreadId());
/*等待内核事件对象状态受信*/
WaitForSingleObject(g_hEvent, INFINITE);//获取权限
for (i = 0; i < clnt_cnt; i++)
{
if (clnt_sock == clnt_socks[i])
{
while (i++ < clnt_cnt - 1)
clnt_socks[i] = clnt_socks[i + 1];
break;
}
}
clnt_cnt--;
SetEvent(g_hEvent); /*设置受信*/
// 关闭同客户端的连接
closesocket(clnt_sock);
return NULL;
}
void send_msg(char* msg, int len)
{
int i;
/*等待内核事件对象状态受信*/
WaitForSingleObject(g_hEvent, INFINITE);
for (i = 0; i < clnt_cnt; i++)
send(clnt_socks[i], msg, len, 0);
SetEvent(g_hEvent); /*设置受信*/
}
这是客户端(client.c)
#include <winsock2.h>
#include <stdio.h>
#include <windows.h>
// 告诉连接器与WS2_32库连接
#pragma comment(lib,"WS2_32.lib")
#define BUF_SIZE 256
#define NAME_SIZE 30
unsigned short port = 80;
typedef struct sockaddr_in sockaddr_in;
typedef struct sockaddr sockaddr;
DWORD WINAPI send_msg(LPVOID lpParam);
DWORD WINAPI recv_msg(LPVOID lpParam);
void error_handling(const char* msg);
char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];
int main()
{
HANDLE hThread[2];
DWORD dwThreadId;
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
WSAStartup(sockVersion, &wsaData);
/*设置登录用户名*/
printf("Input your Chat Name:");
scanf_s("%s", name,20);
getchar(); //接收换行符
// 创建套节字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET)
error_handling("Failed socket()");
// 填写远程地址信息
sockaddr_in servAddr;
memset(&servAddr, '\0', sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(port);
// 如果你的计算机没有联网,直接使用本地地址127.0.0.1
//servAddr.sin_addr.S_un.S_addr = inet_pton("127.0.0.1");
if (inet_pton(AF_INET, "192.168.86.150", &(servAddr.sin_addr.S_un.S_addr)) <= 0)
{
perror("Failed to convert IP address");
exit(1);
}
if (connect(sock, (LPSOCKADDR)&servAddr, sizeof(servAddr)) == -1)
{
int error = WSAGetLastError();
if (error != 0)
{
perror("Failed to connect");
exit(1);
}
}
printf("connect success\n");
hThread[0] = CreateThread(
NULL, // 默认安全属性
NULL, // 默认堆栈大小
send_msg, // 线程入口地址(执行线程的函数)
&sock, // 传给函数的参数
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
hThread[1] = CreateThread(
NULL, // 默认安全属性
NULL, // 默认堆栈大小
recv_msg, // 线程入口地址(执行线程的函数)
&sock, // 传给函数的参数
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
// 等待线程运行结束
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
printf(" Thread Over,Enter anything to over.\n");
getchar();
// 关闭套节字
closesocket(sock);
// 释放WS2_32库
WSACleanup();
return 0;
}
DWORD WINAPI send_msg(LPVOID lpParam)
{
int sock = *((int*)lpParam);
char name_msg[NAME_SIZE + BUF_SIZE];
while (1)
{
fgets(msg, BUF_SIZE, stdin);
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n"))
{
closesocket(sock);
exit(0);
}
sprintf_s(name_msg, sizeof(name_msg), "[%s]: %s", name, msg);
int nRecv = send(sock, name_msg, strlen(name_msg), 0);
}
return NULL;
}
DWORD WINAPI recv_msg(LPVOID lpParam)
{
int sock = *((int*)lpParam);
char name_msg[NAME_SIZE + BUF_SIZE];
int str_len;
while (1)
{
str_len = recv(sock, name_msg, NAME_SIZE + BUF_SIZE - 1, 0);
if (str_len == -1)
return -1;
name_msg[str_len] = 0;
fputs(name_msg, stdout);
}
return NULL;
}
void error_handling(const char* msg)
{
printf("%s\n", msg);
WSACleanup();
exit(1);
}
这样一个简单的聊天界面就跑起来了。效果如下。
以上还涉及fgets()、fputs()函数stdin|stdout是标准输入流和标准输出流。可以通过socket里面的send和recv函数去实现客户端和服务器的数据传输。以上就是今天的学习内容。明天继续。