学了C语言想装x能干点啥?手把手教你写个聊天软件来玩玩

简介: 学了C语言想装x能干点啥?手把手教你写个聊天软件来玩玩

一、服务器


首先来看服务器端,先来搞定几个头文件,不然其中的一些库函数会没法调用:

#pragma once
#include<WinSock2.h>
#include <stdio.h>
#include <stdlib.h>
#include<Windows.h>//必须在<WinSock2.h>的下面包含,否则编译不通过
#pragma comment(lib,"WS2_32.lib")//要包含WinSock2.h必须要包这个库

头文件中的这些库那都是必须要包含的内容,不然之后函数的调用就会出现一堆的报错,下来我们看一下main函数:

//初始化套接字类库 
  //WSAStartup函数用于初始化Ws2_32.dll动态链接库。
  //在使用套接字函数之前,一定要初始化Ws2_32.dll动态链接库 
  WSADATA WsaData = { 0 };
  if (WSAStartup(MAKEWORD(2, 2), &WsaData) != 0)
  {
  return;
  }
  // 创建监听套接字
  SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (ListenSocket == INVALID_SOCKET)
  {
  printf("Failed socket() \n");
  return;
  }

第一件事情就是初始化套接字类库,因为我们需要利用套接字来完成进程间通信,所以类库肯定是要首先初始化的,接下来是创建一个监听套接字,在创建监听套接字的时候需要注意,socket函数中传的参数是非常关键的:

SOCKET WSAAPI socket(
  _In_ int af,//地址家族规范,在这里我们传的是AF_INET 这是IPv4协议规范
  _In_ int type,//这个参数我们传递SOCK_STREAM,可靠的数据流传输,因为TCP协议
  _In_ int protocol//传输控制协议,用的TCP
);

这个函数的三个参数在TCP/IP通信中,基本是固定搭配套餐!当我们把监听套接字创建出来之后,需要将接听套接字与端口绑定:

// 填充sockaddr_in结构
  struct sockaddr_in ServerAddress;
  ServerAddress.sin_family = AF_INET;//Ipv4协议家族
  ServerAddress.sin_port = htons(4567);   //端口号
  ServerAddress.sin_addr.S_un.S_addr = INADDR_ANY;//客户端是本地地址
  // 绑定套接字
  if (bind(ListenSocket, (LPSOCKADDR)&ServerAddress, sizeof(ServerAddress)) == SOCKET_ERROR)
  {
  printf("Failed bind() \n");
  return;
  }

上面的代码中,有一个结构体sockaddr_in其中包含了三个成员,有地址协议家族、监听端口号和监听的地址。其中端口号是随便设置的,只要在端口号范围之内,不要和知名端口号重复就行,我随便写了个4567,保证客户端也连接到这个端口就行!

bind函数是绑定套接字和sockaddr_in结构体,为了让这个套接字可以在该端口和地址协议规范下完成监听,bind函数将本地地址与套接字关联起来。


服务器端完成了套接字端口绑定之后,就要开始监听,listen函数将套接字置于侦听传入连接的状态。可以设置最大的连接数,在这里我随便设置了2。

// 进入监听模式  监听队列  最大连接数设置为 2
  if (listen(ListenSocket, 2) == SOCKET_ERROR)
  {
  printf("Failed listen() \n");
  return;
  }

那监听上线之后,就等着客户端的连接过来,需要一个叫做accept的函数来接受客户端的连接,accept函数允许对套接字的传入连接尝试。在这里设计算是偷了个懒,本应该弄一个循环,因为这是尝试连接,如果连接达到上限,就不允许其它的客户端接入了,应该不断尝试连接。但是这里我们主要为了讲一下实现原理,用于间单的测试还是没问题的

//用于接受客户端连接的IP地址等信息
  struct sockaddr_in ClientAddress;
  int AddressLength = sizeof(ClientAddress);//计算这个长度在accept处使用
  SOCKET ClientSocket;
  printf("等待客户端连接:\n");
  // 接受一个新连接
  ClientSocket = accept(ListenSocket, (SOCKADDR*)&ClientAddress, &AddressLength);
  if (ClientSocket == INVALID_SOCKET)
  {
  printf("Failed accept()");
  }

客户端和服务器连接成功之后,我们创建一个线程,在线程创建过程中,把客户端的Socket当作参数传递给线程,这个线程用于给客户端发送消息:

printf("接收到连接:%s \r\n", inet_ntoa(ClientAddress.sin_addr));
  HANDLE ThreadHandle = CreateThread(NULL,
  0,
  (LPTHREAD_START_ROUTINE)ThreadProcedure,
  &ClientSocket,
  0,
  NULL);
  if (ThreadHandle == NULL)
  {
  return 0;
  }

在这个回调线程执行函数中,用于和客户端通信,用gets来读取数据,遇到回车读取结束,然后只要保持连接,就可以一直给客户端发送消息,如果想断开连接,输入Over即可。

//相当于一个发送消息模块
DWORD WINAPI ThreadProcedure(LPVOID Parameter)
{
  SOCKET ClientSocket;
  char BufferData[260];//最大发送的字符数
  ClientSocket = *(SOCKET*)Parameter;
  printf("You can speak now:\n");
  while (1)
  {
  memset(BufferData, 0, sizeof(BufferData));
  gets(BufferData);
  // 向客户端发送数据
  send(ClientSocket, BufferData, strlen(BufferData), 0);
  if (!strncmp(BufferData, "Over", strlen("Over")))
  {
    // 关闭同客户端的连接 退出程序
    closesocket(ClientSocket);
    exit(0);
  }
  }
  return 0;
}

在异步线程可以发送消息的同时,主线程也没闲着,它在接收客户端的数据发送,也是在一个while循环中,一直接受者来自客户端的消息,直到客户端发出Over指示,断开连接:

//用于接收数据
  char BufferData[260];
  while (TRUE)
  {
  memset(BufferData, 0, sizeof(BufferData));
  recv(ClientSocket, BufferData, sizeof(BufferData), 0);
  if (!strncmp(BufferData, "Over", strlen("Over")))
  {
    CloseHandle(ThreadHandle);
    ThreadHandle = NULL;
    break;
  }
  printf("Client Said: %s\n", BufferData);
  }

到这里,一个简单的服务器端就搞定了!!!接下来我们看一下客户端的实现吧:


二、客户端


客户端的代码实现逻辑其实和服务器端是相当接近的,我们需要包含的头文件也没有变化:

#pragma once
#include<WinSock2.h>
#include <stdio.h>
#include <stdlib.h>
#include<Windows.h>//必须在<WinSock2.h>的下面包含,否则编译不通过
#pragma comment(lib,"WS2_32.lib")//要包含WinSock2.h必须要包这个库

这些头文件都是必须包含的,在之前就已经说过了,因为实现逻辑很接近,所以我就找那些不太一样的地方来给大家解释一下:


一上来那肯定是main函数了,里面还是一样,初始化类库,创建套接字:

//初始化套接字类库 
  //WSAStartup函数用于初始化Ws2_32.dll动态链接库。在使用套接字函数之前,一定要初始化Ws2_32.dll动态链接库 
  WSADATA v1 = { 0 };
  if (WSAStartup(MAKEWORD(2, 2), &v1) != 0)
  {
  return;
  }
  // 创建套接字
  SOCKET CommunicateSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (CommunicateSocket == INVALID_SOCKET)
  {
  printf(" Failed socket() \n");
  return;
  }

然后我们需要声明并且给sockaddr_in结构体赋值,这里有所不同,对于地址协议家族和端口号来说是一样的,尤其端口号,肯定要和服务器保持一致,然后我们讲连接的地址写为“127.0.0.1”,这是连接到本地的IP地址,在本机方便测试:

// 填写远程地址信息
  struct sockaddr_in ServerAddress;
  ServerAddress.sin_family = AF_INET;
  ServerAddress.sin_port = htons(4567);
  //此处直接使用127.0.0.1即可 就是连接到本机
  ServerAddress.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

初始化结束之后,我们使用connect将客户端的套接字通过这个IP和端口来和服务器进行连接,connect函数建立到指定套接字的连接:

if (connect(CommunicateSocket, (SOCKADDR*)&ServerAddress, sizeof(ServerAddress)) == SOCKET_ERROR)
  {
  printf(" Failed connect() \n");
  return;
  }
  if (CommunicateSocket == INVALID_SOCKET)
  {
  printf("Failed accept()");
  }

一旦连接成功,继续创建线程,这个线程传的是当前与服务器连接起来的CommunicateSocket,这个套接字就是客户端和服务器交流的桥梁:

printf("连接成功!!\r\n");
  HANDLE ThreadHandle = CreateThread(NULL,
  0,
  (LPTHREAD_START_ROUTINE)ThreadProcedure,
  &CommunicateSocket,
  0,
  NULL);
  if (ThreadHandle == NULL)
  {
  return 0;
  }

创建异步线程还是一样的,将当前传入的套接字,用于给服务器发送消息,发送Over来结束当前会话:

//向服务器发送消息
DWORD WINAPI ThreadProcedure(LPVOID Parameter)
{
  SOCKET ServerSocket;
  char BufferData[260];
  ServerSocket = *(SOCKET*)Parameter;
  printf("You can speak now:\n");
  while (1)
  {
  memset(BufferData, 0, sizeof(BufferData));
  gets(BufferData);
  // 向服务器发送数据
  send(ServerSocket, BufferData, strlen(BufferData), 0);
  if (!strncmp(BufferData, "Over", strlen("Over")))
  {
    // 关闭同服务器的连接 退出程序
    closesocket(ServerSocket);
    exit(0);
  }
  }
  return 0;
}

那main函数中的主线程肯定还是接受来自服务器端的消息,除非遇到Over指令,来结束对话:

//接受来自服务器的消息
  char BufferData[260];
  while (TRUE)
  {
  memset(BufferData, 0, sizeof(BufferData));
  recv(CommunicateSocket, BufferData, sizeof(BufferData), 0);
  if (!strncmp(BufferData, "Over", strlen("Over")))
  {
    CloseHandle(ThreadHandle);
    ThreadHandle = NULL;
    break;
  }
  printf("Server Said: %s\n", BufferData);
  }

到这里,实现就基本结束了,记得代码中断开连接之后,最后关闭套接字。代码记得首先启动服务器,这样才能达到监听的效果,让客户端顺利连接,我们来用动态图演示一下效果:

image.png


如果觉得文章不错,麻烦给个点赞+评论+收藏支持一下🤞🤞🤞。代码实现其实在文章中已经够详细了,但是如果有需要源码的,可以找我要。感谢您的阅读!


目录
相关文章
|
Unix Linux C语言
Linux下C语言多线程,网络通信简单聊天程序
原文:Linux下C语言多线程,网络通信简单聊天程序 功能描述:程序应用多线程技术,可是实现1对N进行网络通信聊天。但至今没想出合适的退出机制,除了用Ctr+C。出于演示目的,这里采用UNIX域协议(文件系统套接字),程序分为客户端和服务端。
1020 0
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
41 3
|
23天前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
46 10
|
23天前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
43 9
|
23天前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
33 8
|
23天前
|
C语言 开发者
【C语言】数学函数详解
在C语言中,数学函数是由标准库 `math.h` 提供的。使用这些函数时,需要包含 `#include <math.h>` 头文件。以下是一些常用的数学函数的详细讲解,包括函数原型、参数说明、返回值说明以及示例代码和表格汇总。
43 6
|
23天前
|
存储 C语言
【C语言】输入/输出函数详解
在C语言中,输入/输出操作是通过标准库函数来实现的。这些函数分为两类:标准输入输出函数和文件输入输出函数。
153 6
|
23天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
52 6
|
23天前
|
C语言 开发者
【C语言】断言函数 -《深入解析C语言调试利器 !》
断言(assert)是一种调试工具,用于在程序运行时检查某些条件是否成立。如果条件不成立,断言会触发错误,并通常会终止程序的执行。断言有助于在开发和测试阶段捕捉逻辑错误。
35 5
|
1月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
56 4