写在前面
前面写过相关的文章,但是那是很早之前了,那时刚开始接触编程不久,完全是处于冲动想完成一个小项目,但是实际完成过程中并没有收获很多,完全是从各种博客以及别人的代码模仿而写的一个通信程序,这次重写关于这块的文章是为了重新学习,对后面通信的学习的有个更好的理解,也希望各位看到我的简陋文章能有一定的收获。
函数接口介绍
首先是关于各种套接字编程的API接口,这些接口有许多人都能介绍的比我好,所以我也不一一叙述,但是想认真学习一下的就去看看UNIX环境高级编程中的这一块,毕竟Windows和Linux都是在这基础上的。大家在学习各种函数接口时关注传入的各个参数以及返回值,许多文章都只介绍了函数作用以及传参,对返回值没有提及,但是我们作为开发人员调用函数就是为了得到一个结果,所以学习时要多多关注返回值。
实现思路
我相信正在学习Socket的小伙伴许多都应该和我一样是一个初学者,所以作为一个初学者,我们的思路应该在代码之前,不要急着去实现,先想清楚用哪些技术,怎样的方案去实现怎样的功能,不要一开始就着急实现,结果后来又需要更改许多。
个人对于服务端的实现思路:
1.循环监听接入的客户端,接入的客户端有一个Socket描述符。
2.1.对每一个成功接入的客户端开启一个线程去处理接入的客户端的请求或转发要求。
2.2.采用多路复用的方式去处理接入的客户端发送过来的请求。
3.客户端关闭的要及时关闭套接字以及将线程释放。
个人对于客户端的实现思路:
1.我们都知道输入流函数是一个阻塞函数,我们要想实现聊天那么必然要有输入,既然要输入又不想阻塞整个进程只有将输入函数放到一个线程中去处理。
2.我们同时还需要不断的去接收别人发送的信息,可以在主线程中完成。
3.对于发送和接收的信息,我们必须有个约定去处理粘包等问题,有许多方法去实现,大家可以自行百度寻找一种自己觉得适合的。
Client:双线程,一个线程处理输入,一个线程处理接收
#include <iostream>
#include <string>
#include <queue>
#include <thread>
#include <mutex>
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
using namespace std;
//存放键入的值
queue<string> q;
//定义一把锁
mutex m;
//定义一个缓冲区
string bufferString;
//定义协议头尾识别符
string protocol(string message) {
//string len = to_string(message.length());
string message1 = "start#" + message + "#end";
return message1;
}
//接收键盘输入线程
void thread1() {
while (true) {
//向服务端发送数据
char str[255];
//cout << "输入要发送的内容:" << endl;
cin.getline(str, 255);
string str1 = protocol(str);
//队尾添加元素
m.lock();
q.push(str1);
m.unlock();
}
}
//处理接收的信息
string handleMessage(string message) {
//cout << "Message form server: " << message << endl;
bufferString = bufferString + message;
int s = bufferString.find("start#");
int e = bufferString.find("#end");
string str;
if (s != string::npos && e != string::npos) {
int m = bufferString.find(" ");
str = bufferString.substr(s + m + 1, e - s - m-1);
bufferString = bufferString.substr(e+4);
}
return str;
}
int main() {
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
unsigned long Opt = 1;
//创建套接字
//domain 网络通信的域 协议族 IPv4协议
//type 套接字通信类型 使用Tcp协议
//protocol 对应协议 一般设置为0自动匹配
SOCKET sock = socket(PF_INET, SOCK_STREAM, 0);
//向服务器发起请求
struct sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //选用IPv4
sockAddr.sin_addr.s_addr = inet_addr("172.16.8.12"); //服务器ip地址
sockAddr.sin_port = htons(8000); //端口号
if (connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)) == SOCKET_ERROR){
cout << "connect error!" << endl;
return 0;
}
//开启一个线程处理发送信息
thread t1(thread1);
//轮询发送接收服务器传回的数据
while (true) {
if (!q.empty()) {
string str = q.front();
//删除队首元素
m.lock();
q.pop();
m.unlock();
const char *sendMessage = str.c_str();
send(sock, sendMessage, strlen(sendMessage), NULL);
}
char recvMessage[255];
memset(recvMessage, 0, 255); //每个字节都用0填充
ioctlsocket(sock, FIONBIO, &Opt); //非阻塞模式
//输出接收到的数据
int n;
if ((n = recv(sock, recvMessage, 255, 0)) > 0) {
cout << "n:" << n << endl;
cout << "recvMessage: " << recvMessage << endl;
//cout << handleMessage(recvMessage) << endl;
//cout << "Message form server: " << recvMessage << endl;
}
}
//关闭套接字
closesocket(sock);
//终止使用 DLL
WSACleanup();
system("pause");
return 0;
}
Server1:多线程,接入一个客户端则开启一个线程处理
#include <iostream>
#include <string>
#include <thread>
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
using namespace std;
//unsigned long Opt = 1;
int fd[100];
//定义协议头尾识别符
string protocol(string str) {
string message = "start#" + str + "#end";
return message;
}
//向客户端转发数据
void threadSend(SOCKET sock,char *message) {
//const char *str="aaaaaa";
//cout << "输入要发送的内容:";
//cin.getline(str, 20);
//string str1 = protocol(str);
send(sock, message, strlen(message) + sizeof(char), NULL);
}
//接收客户端发来的数据
int threadRecv(SOCKET sock) {
char message[MAXBYTE];
while (true) {
::memset(message, 0, sizeof(message)); //每个字节都用0填充
//ioctlsocket(sock, FIONBIO, &Opt); //非阻塞模式
int n = recv(sock, message, 255, NULL);
if (n > 0) {
int id = message[5] - '0';
SOCKET sendSock = fd[id];
threadSend(sendSock, message);
}else{
closesocket(sock);
for (int i = 1; i < 100; i++) {
if (fd[i] == sock) {
cout << "客户端:" << i << " 下线" << endl;
}
}
break;
}
}
return 0;
}
int main() {
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET slisten = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); //监听套接字
if (slisten == SOCKET_ERROR) {
cout << "creat error!" << endl;
return 0;
}
//绑定套接字
struct sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr("172.16.8.12"); //具体的IP地址
sockAddr.sin_port = htons(8080); //端口
if (::bind(slisten, (SOCKADDR*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
cout << "bind error!" << endl;
return 0;
}
//进入监听状态
if (listen(slisten, 20) == SOCKET_ERROR) {
cout << "listen error!" << endl;
return 0;
}
//循环接收客户端请求
int id = 1;
sockaddr_in clientAddr; //用于接收客户端协议地址
int nAddrlen = sizeof(clientAddr); //用于接收客户端协议地址长度
SOCKET clientSock; //接收的客户端的套接字
while (true) {
//cout << "等待连接..." << endl;
clientSock = accept(slisten, (SOCKADDR*)&clientAddr, &nAddrlen);
if (clientSock == SOCKET_ERROR) {
cout << "accept error" << endl;
//关闭套接字
closesocket(clientSock);
continue;
}
cout << "客户端:" << id << " 接入" << endl;
struct sockaddr_in sa;
int len = sizeof(sa);
/*
if (!getpeername(clientSock, (struct sockaddr *)&sa, &len)) {
cout << "IP: " << inet_ntoa(sa.sin_addr) << endl;
cout << "Ports: " << ntohs(sa.sin_port) << endl;
}
if (!getsockname(clientSock, (struct sockaddr *)&sa, &len)) {
cout << "IP: " << inet_ntoa(sa.sin_addr) << endl;
cout << "Ports: " << ntohs(sa.sin_port) << endl;
}
*/
thread a(threadRecv,clientSock);
a.detach();
string str = to_string(id) + "your id is:" + to_string(id);
string str1 = protocol(str);
char *message = (char *)str1.c_str();
threadSend(clientSock, message);
fd[id] = clientSock;
id++;
}
//关闭监听套接字
closesocket(slisten);
//终止 DLL 的使用
WSACleanup();
system("pause");
return 0;
}
Server2:单线程采用多路复用的方法实现多客户端接入
#include <iostream>
#include <string>
#include <thread>
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
using namespace std;
//定义协议头尾识别符
string protocol(string str) {
string message = "start#" + str + "#end";
return message;
}
//向客户端转发数据
void threadSend(SOCKET sock, char *message) {
int len = strlen(message);
//cout << "len:" << len << endl;
int n = send(sock, message, strlen(message), NULL);
}
int main() {
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET slisten = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); //监听套接字
if (slisten == SOCKET_ERROR) {
cout << "creat error!" << endl;
return 0;
}
//绑定套接字
struct sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr("172.16.8.12"); //具体的IP地址
sockAddr.sin_port = htons(8000); //端口
if (::bind(slisten, (SOCKADDR*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
cout << "bind error!" << endl;
return 0;
}
//进入监听状态
if (listen(slisten, 20) == SOCKET_ERROR) {
cout << "listen error!" << endl;
return 0;
}
fd_set fdSocket;
FD_ZERO(&fdSocket);
FD_SET(slisten, &fdSocket);//将sListen添加进该集合
int fd[100];
bool flag[100] = {false};
int id = 1;
string stringBuffer[100];
string offlineBuffer[100];
while (true)
{
fd_set fdRead = fdSocket;
int nRet = select(NULL, &fdRead, NULL, NULL, NULL);
if (nRet <= 0)
break;
for (int i = 0; i < (int)fdSocket.fd_count; ++i)
{
if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
{
if (fdSocket.fd_array[i] == slisten)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
SOCKET sNew = ::accept(slisten, (sockaddr*)&addrRemote, &nAddrLen);
FD_SET(sNew, &fdSocket);
cout << "Client " << sNew << " connected" << endl;
string str = to_string(sNew) + " your id is:" + to_string(id);
fd[id] = sNew;
flag[id] = true;
string str1 = protocol(str);
char *message = (char *)str1.c_str();
threadSend(sNew, message);
//上线处理缓存区内容
do {
int s = offlineBuffer[id].find("start#");
int e = offlineBuffer[id].find("#end");
if (s != string::npos && e != string::npos) {
int m = offlineBuffer[id].find(" ");
//cout << "id:" << id << endl;
string str = offlineBuffer[id].substr(s, e + 4).data();
char* message = (char*)str.data();
threadSend(fd[id], message);
offlineBuffer[id] = offlineBuffer[id].substr(e + 4);
}
} while (offlineBuffer[id].find("start#") != string::npos && offlineBuffer[id].find("#end") != string::npos);
id++;
}
else
{
char buffer[1024];
memset(buffer, 0, 1024);
int nRecev = recv(fdSocket.fd_array[i], buffer, 1024, 0);
int idRecv;
for (int j = 1; j < 100; j++) {
if (fd[j] == fdSocket.fd_array[i]) {
idRecv = j;
}
}
if (nRecev > 0)
{
stringBuffer[idRecv] = stringBuffer[idRecv] + buffer;
//处理分包,粘包问题
int idSend;
do {
int s = stringBuffer[idRecv].find("start#");
int e = stringBuffer[idRecv].find("#end");
if (s != string::npos && e != string::npos) {
int m = stringBuffer[idRecv].find(" ");
string iSend = stringBuffer[idRecv].substr(6, m - 6);
idSend = atoi(iSend.c_str());
//cout << "idSend: " << idSend << endl;
string str = stringBuffer[idRecv].substr(s,e+4).data();
if (flag[idSend]) {
char* message = (char*)str.data();
threadSend(fd[idSend], message);
}
else {
offlineBuffer[idSend] = offlineBuffer[idSend] + str;
//cout << "offlineBuffer[" << idSend << "]" << offlineBuffer[idSend] << endl;
}
stringBuffer[idRecv] = stringBuffer[idRecv].substr(e + 4);
//cout << "stringBuffer[" << idRecv << "]:" << stringBuffer[idRecv] << endl;
}
} while (stringBuffer[idRecv].find("start#") != string::npos && stringBuffer[idRecv].find("#end") != string::npos);
}
else
{
closesocket(fdSocket.fd_array[i]);
flag[idRecv] = false;
FD_CLR(fdSocket.fd_array[i], &fdSocket);
cout << "客户端:" << idRecv << "下线" << endl;
}
}
}
}
}
//关闭监听套接字
closesocket(slisten);
//终止 DLL 的使用
WSACleanup();
system("pause");
return 0;
}
功能描述
第一个服务端的程序先写的,可能与客户端有点不对应了,有一定的参考价值。第二个服务端程序是按照接入的第一个客户端默认为id=1,后面接入的客户端一次增加id号,每次客户端发送信息必须将目标的id写入报文中,即 "id message"的格式,
写在最后
整个代码其实很简单,其实本来是不想将代码放出来丢人的,因为代码其实很简单,就只是一个小demo,代码中有许多的问题可寻(但是我也不想去做后面的内容了),但是后来想想可能我的文章都不会有多少人看,那么放出来也无妨,也希望能对正在学习这部分的同学有一定的参考学习价值。愿星光不负赶路人,每个坚持学习的人都熠熠生辉。