socket多人聊天程序C语言版(一)

时间:2021-10-12 23:43:05

首先,不要一步登天直接解决多人聊天这个问题,先把问题化简。
1.多人聊天的核心问题是服务器如何标识不同的客户端,如何根据客户端的需求转发消息给指定客户端。
2.多人聊天转化为C-C聊天,但是不再是直接C-C,而是通过server转发消息,所以变成==>C-S-C。
3.server如何允许2个client同时连接,设置listen函数的第二个参数,最大连接数。
4.server如何标识两个client,用一个结构体数组来存放两个client的信息。
5.server如何转发消息给client,很简单,先接收到的发送给还没接收到的。如图:

socket多人聊天程序C语言版(一)

6.server如何管理两个client的连接状态,连接成功很简单,就是accpet成功后就是连接成功了。但是怎么判断连接断开呢?这个涉及到的select函数的使用,有点复杂~,所以我就简单的用了一个send函数发送一个空消息来判断是否断开连接,这个不严谨,容易出BUG,但是实践起来简单就使用了它。
7.要用线程来管理接收消息、发送消息、接受请求、管理连接状态。

技术要点:C语言线程函数的使用。

?
1
2
3
4
5
6
7
8
9
10
_beginthreadex函数原型
 
_ACRTIMP uintptr_t __cdecl _beginthreadex
( _In_opt_ void* _Security,//安全属性,NULL为默认安全属性
 _In_ unsigned _StackSize,//线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0
 _In_ _beginthreadex_proc_type _StartAddress, //线程函数的地址
 _In_opt_ void* _ArgList, //传进线程的函数
 _In_ unsigned _InitFlag, //线程初始状态,0:立即运行;CREATE_SUSPEND:悬挂(如果出事状态定义为悬挂,就要调用ResumeThread(HANDLE) 来激活线程的运行)
 _Out_opt_ unsigned* _ThrdAddr //用于记录线程ID的地址
 )

例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <process.h>
#include <stdio.h>
 
unsigned __stdcall Thread(void* param)
{
  printf("%d\n", *(int*)param); //这里必须先要强行转换为int*,不然void* 直接解引用会出错。
  return 0;
}
 
int main()
{
  int i = 0;
  _beginthreadex(NULL, 0, Thread, &i, 0, NULL);
  return 0;
}

1V1,C-S-C聊天例子:
编写环境:win10,VS2015

效果图:

socket多人聊天程序C语言版(一)

socket多人聊天程序C语言版(一)

socket多人聊天程序C语言版(一)

server code:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#include <WinSock2.h>
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
#pragma comment(lib,"ws2_32.lib")
 
#define SEND_OVER 1             //已经转发消息
#define SEND_YET 0             //还没转发消息
 
int g_iStatus = SEND_YET;
SOCKET g_ServerSocket = INVALID_SOCKET;   //服务端套接字
SOCKADDR_IN g_ClientAddr = { 0 };      //客户端地址
int g_iClientAddrLen = sizeof(g_ClientAddr);
bool g_bCheckConnect = false;        //检查连接情况
HANDLE g_hRecv1 = NULL;
HANDLE g_hRecv2 = NULL;
//客户端信息结构体
typedef struct _Client
{
  SOCKET sClient;   //客户端套接字
  char buf[128];    //数据缓冲区
  char userName[16];  //客户端用户名
  char IP[20];     //客户端IP
  UINT_PTR flag;    //标记客户端,用来区分不同的客户端
}Client;
 
Client g_Client[2] = { 0 };         //创建一个客户端结构体
 
//发送数据线程
unsigned __stdcall ThreadSend(void* param)
{
  int ret = 0;
  int flag = *(int*)param;
  SOCKET client = INVALID_SOCKET;         //创建一个临时套接字来存放要转发的客户端套接字
  char temp[128] = { 0 };             //创建一个临时的数据缓冲区,用来存放接收到的数据
  memcpy(temp, g_Client[!flag].buf, sizeof(temp));
  sprintf(g_Client[flag].buf, "%s: %s", g_Client[!flag].userName, temp);//添加一个用户名头
 
  if (strlen(temp) != 0 && g_iStatus == SEND_YET) //如果数据不为空且还没转发则转发
    ret = send(g_Client[flag].sClient, g_Client[flag].buf, sizeof(g_Client[flag].buf), 0);
  if (ret == SOCKET_ERROR)
    return 1;
  g_iStatus = SEND_OVER;  //转发成功后设置状态为已转发
  return 0;
}
 
//接受数据
unsigned __stdcall ThreadRecv(void* param)
{
  SOCKET client = INVALID_SOCKET;
  int flag = 0;
  if (*(int*)param == g_Client[0].flag)      //判断是哪个客户端发来的消息
  {
    client = g_Client[0].sClient;
    flag = 0;
  
  else if (*(int*)param == g_Client[1].flag)
  {
    client = g_Client[1].sClient;
    flag = 1;
  }
  char temp[128] = { 0 }; //临时数据缓冲区
  while (1)
  {
    memset(temp, 0, sizeof(temp));
    int ret = recv(client, temp, sizeof(temp), 0); //接收数据
    if (ret == SOCKET_ERROR)
      continue;
    g_iStatus = SEND_YET;                //设置转发状态为未转发
    flag = client == g_Client[0].sClient ? 1 : 0;    //这个要设置,否则会出现自己给自己发消息的BUG
    memcpy(g_Client[!flag].buf, temp, sizeof(g_Client[!flag].buf));
    _beginthreadex(NULL, 0, ThreadSend, &flag, 0, NULL); //开启一个转发线程,flag标记着要转发给哪个客户端
    //这里也可能是导致CPU使用率上升的原因。
  }
 
  return 0;
}
 
//管理连接
unsigned __stdcall ThreadManager(void* param)
{
  while (1)
  {
    if (send(g_Client[0].sClient, "", sizeof(""), 0) == SOCKET_ERROR)
    {
      if (g_Client[0].sClient != 0)
      {
        CloseHandle(g_hRecv1); //这里关闭了线程句柄,但是测试结果断开连C/S接后CPU仍然疯涨
        CloseHandle(g_hRecv2);
        printf("Disconnect from IP: %s,UserName: %s\n", g_Client[0].IP, g_Client[0].userName);
        closesocket(g_Client[0].sClient);  //这里简单的判断:若发送消息失败,则认为连接中断(其原因有多种),关闭该套接字
        g_Client[0] = { 0 };
      }
    }
    if (send(g_Client[1].sClient, "", sizeof(""), 0) == SOCKET_ERROR)
    {
      if (g_Client[1].sClient != 0)
      {
        CloseHandle(g_hRecv1);
        CloseHandle(g_hRecv2);
        printf("Disconnect from IP: %s,UserName: %s\n", g_Client[1].IP, g_Client[1].userName);
        closesocket(g_Client[1].sClient);
        g_Client[1] = { 0 };
      }
    }
    Sleep(2000); //2s检查一次
  }
 
  return 0;
}
 
//接受请求
unsigned __stdcall ThreadAccept(void* param)
{
 
  int i = 0;
  int temp1 = 0, temp2 = 0;
  _beginthreadex(NULL, 0, ThreadManager, NULL, 0, NULL);
  while (1)
  {
    while (i < 2)
    {
      if (g_Client[i].flag != 0)
      {
        ++i;
        continue;
      }
      //如果有客户端申请连接就接受连接
      if ((g_Client[i].sClient = accept(g_ServerSocket, (SOCKADDR*)&g_ClientAddr, &g_iClientAddrLen)) == INVALID_SOCKET)
      {
        printf("accept failed with error code: %d\n", WSAGetLastError());
        closesocket(g_ServerSocket);
        WSACleanup();
        return -1;
      }
      recv(g_Client[i].sClient, g_Client[i].userName, sizeof(g_Client[i].userName), 0); //接收用户名
      printf("Successfuuly got a connection from IP:%s ,Port: %d,UerName: %s\n",
        inet_ntoa(g_ClientAddr.sin_addr), htons(g_ClientAddr.sin_port), g_Client[i].userName);
      memcpy(g_Client[i].IP, inet_ntoa(g_ClientAddr.sin_addr), sizeof(g_Client[i].IP)); //记录客户端IP
      g_Client[i].flag = g_Client[i].sClient; //不同的socke有不同UINT_PTR类型的数字来标识
      i++;
    }
    i = 0;
 
    if (g_Client[0].flag != 0 && g_Client[1].flag != 0)         //当两个用户都连接上服务器后才进行消息转发
    {
      if (g_Client[0].flag != temp1)   //每次断开一个连接后再次连上会新开一个线程,导致cpu使用率上升,所以要关掉旧的
      {
        if (g_hRecv1)         //这里关闭了线程句柄,但是测试结果断开连C/S接后CPU仍然疯涨
          CloseHandle(g_hRecv1);
        g_hRecv1 = (HANDLE)_beginthreadex(NULL, 0, ThreadRecv, &g_Client[0].flag, 0, NULL); //开启2个接收消息的线程
      
      if (g_Client[1].flag != temp2)
      {
        if (g_hRecv2)
          CloseHandle(g_hRecv2);
        g_hRecv2 = (HANDLE)_beginthreadex(NULL, 0, ThreadRecv, &g_Client[1].flag, 0, NULL);
      }   
    }
 
    temp1 = g_Client[0].flag; //防止ThreadRecv线程多次开启
    temp2 = g_Client[1].flag;
 
    Sleep(3000);
  }
 
  return 0;
}
 
//启动服务器
int StartServer()
{
  //存放套接字信息的结构
  WSADATA wsaData = { 0 };
  SOCKADDR_IN ServerAddr = { 0 };       //服务端地址
  USHORT uPort = 18000;            //服务器监听端口
 
  //初始化套接字
  if (WSAStartup(MAKEWORD(2, 2), &wsaData))
  {
    printf("WSAStartup failed with error code: %d\n", WSAGetLastError());
    return -1;
  }
  //判断版本
  if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
  {
    printf("wVersion was not 2.2\n");
    return -1;
  }
  //创建套接字
  g_ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (g_ServerSocket == INVALID_SOCKET)
  {
    printf("socket failed with error code: %d\n", WSAGetLastError());
    return -1;
  }
 
  //设置服务器地址
  ServerAddr.sin_family = AF_INET;//连接方式
  ServerAddr.sin_port = htons(uPort);//服务器监听端口
  ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//任何客户端都能连接这个服务器
 
  //绑定服务器
  if (SOCKET_ERROR == bind(g_ServerSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)))
  {
    printf("bind failed with error code: %d\n", WSAGetLastError());
    closesocket(g_ServerSocket);
    return -1;
  }
  //设置监听客户端连接数
  if (SOCKET_ERROR == listen(g_ServerSocket, 20000))
  {
    printf("listen failed with error code: %d\n", WSAGetLastError());
    closesocket(g_ServerSocket);
    WSACleanup();
    return -1;
  }
 
  _beginthreadex(NULL, 0, ThreadAccept, NULL, 0, 0);
  for (int k = 0;k < 100;k++) //让主线程休眠,不让它关闭TCP连接.
    Sleep(10000000);
 
  //关闭套接字
  for (int j = 0;j < 2;j++)
  {
    if (g_Client[j].sClient != INVALID_SOCKET)
      closesocket(g_Client[j].sClient);
  }
  closesocket(g_ServerSocket);
  WSACleanup();
  return 0;
}
 
int main()
{
  StartServer(); //启动服务器
 
  return 0;
}

client code:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <WinSock2.h>
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#pragma comment(lib,"ws2_32.lib")
#define RECV_OVER 1
#define RECV_YET 0
char userName[16] = { 0 };
int iStatus = RECV_YET;
//接受数据
unsigned __stdcall ThreadRecv(void* param)
{
  char buf[128] = { 0 };
  while (1)
  {
    int ret = recv(*(SOCKET*)param, buf, sizeof(buf), 0);
    if (ret == SOCKET_ERROR)
    {
      Sleep(500);
      continue;
    }
    if (strlen(buf) != 0)
    {
      printf("%s\n", buf);
      iStatus = RECV_OVER;
    }
    else
      Sleep(100); 
 
 
  }
  return 0;
}
 
//发送数据
unsigned __stdcall ThreadSend(void* param)
{
  char buf[128] = { 0 };
  int ret = 0;
  while (1)
  {
    int c = getch();
    if(c == 72 || c == 0 || c == 68)//为了显示美观,加一个无回显的读取字符函数
      continue;          //getch返回值我是经过实验得出如果是返回这几个值,则getch就会自动跳过,具体我也不懂。
    printf("%s: ", userName);
    gets_s(buf);
    ret = send(*(SOCKET*)param, buf, sizeof(buf), 0);
    if (ret == SOCKET_ERROR)
      return 1;
  }
  return 0;
}
 
//连接服务器
int ConnectServer()
{
  WSADATA wsaData = { 0 };//存放套接字信息
  SOCKET ClientSocket = INVALID_SOCKET;//客户端套接字
  SOCKADDR_IN ServerAddr = { 0 };//服务端地址
  USHORT uPort = 18000;//服务端端口
             //初始化套接字
  if (WSAStartup(MAKEWORD(2, 2), &wsaData))
  {
    printf("WSAStartup failed with error code: %d\n", WSAGetLastError());
    return -1;
  }
  //判断套接字版本
  if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
  {
    printf("wVersion was not 2.2\n");
    return -1;
  }
  //创建套接字
  ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (ClientSocket == INVALID_SOCKET)
  {
    printf("socket failed with error code: %d\n", WSAGetLastError());
    return -1;
  }
  //输入服务器IP
  printf("Please input server IP:");
  char IP[32] = { 0 };
  gets_s(IP);
  //设置服务器地址
  ServerAddr.sin_family = AF_INET;
  ServerAddr.sin_port = htons(uPort);//服务器端口
  ServerAddr.sin_addr.S_un.S_addr = inet_addr(IP);//服务器地址
 
  printf("connecting......\n");
  //连接服务器
  if (SOCKET_ERROR == connect(ClientSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)))
  {
    printf("connect failed with error code: %d\n", WSAGetLastError());
    closesocket(ClientSocket);
    WSACleanup();
    return -1;
  }
  printf("Connecting server successfully IP:%s Port:%d\n",
    IP, htons(ServerAddr.sin_port));
  printf("Please input your UserName: ");
  gets_s(userName);
  send(ClientSocket, userName, sizeof(userName), 0);
  printf("\n\n");
  _beginthreadex(NULL, 0, ThreadRecv, &ClientSocket, 0, NULL); //启动接收和发送消息线程
  _beginthreadex(NULL, 0, ThreadSend, &ClientSocket, 0, NULL);
  for (int k = 0;k < 1000;k++)
    Sleep(10000000);
  closesocket(ClientSocket);
  WSACleanup();
  return 0;
}
 
int main()
{
  ConnectServer(); //连接服务器
  return 0;
}

这程序还有一些BUG,其中最大的就是关掉一个连接后CPU使用率疯涨,我测试过我想到的可能,还是找不到结果~,希望有大神懂的告知一下。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。