学了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域协议(文件系统套接字),程序分为客户端和服务端。
1016 0
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
6天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
22 6
|
26天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
34 10
|
20天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
25天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
53 7
|
25天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
29 4
|
30天前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。
|
1月前
|
C语言
c语言回顾-函数递归(上)
c语言回顾-函数递归(上)
31 2
|
1月前
|
Java 编译器 C语言
【一步一步了解Java系列】:Java中的方法对标C语言中的函数
【一步一步了解Java系列】:Java中的方法对标C语言中的函数
21 3