网络编程(socket): 基于tcp的服务端与客户端聊天小程序
一、基础概念
1、网络架构
Client/Server结构(C/S结构)客户机和服务器结构。本文的主角。B/S结构(Browser/Server,浏览器/服务器模式),WEB浏览器是客户端最主要的应用软件。
2、IP
IP地址是网路通信寻址的主要手段
3、端口(port )
每台计算机有很多个端口。通常是一个进程(运行着的程序)对应一个端口,访问该主机的某个端口就是访问对应的进程。有些端口是默认的对应一些进程,像其中80端口分配给WWW服务,21端口分配给FTP服务。通常我们选用1024以上的端口。
4、socket(套接字)
套接字是一个很抽象的概念。可以理解为连接两个端口之间一条虚拟的线,而端口就是虚拟的接口,或者可以理解为电源线跟插头的关系。要进行网络通信,少了它不行。
二、c/s架构
s是服务器(运行了服务端程序的计算机),c是客户机。c向s发送消息(请求),s接受到消息并处理(响应)。建立连接后s也可以主动向c发送消息,通常是多个c对应一个s。这里我用的只有一个客户端。
三、tcp通信
1、连接通信
在进行通信前,需要做一个虚连接。即客户端c想要与服务端s进行通信必须先要与之进行连接。
2、客户端
客户端通信很简单,只要取得与服务端的联系就可以进行信息的收发。
3、服务端
服务端是一个进程,总是等待着客户端发来请求,并处理相应的请求。
四、通信过程
不涉及具体调用函数的说明。
1、客户端设计
第一步、确定要进行连接的服务端的信息(IP、port)
第二步、获取一个socket,调用相应的函数得到
第三步、用得到的socket与服务端的信息去调用connect与服务端进行连接
第四部、收发消息(send/recv)
第五步、关闭打开的套接字
2、服务端的设计
第一步、确定本机IP和要与程序绑定的端口
第二步、获取一个监听用的socket。
第三步、将得到的socket与确定后的端口绑定,调用bind.
第四步、监听。坐等客户端的到来(没来是一个处于阻塞的函数,调用 listen)
第五步、接受客户端的请求,并建立一个新的套接字来与客户端通信(accept)
第六步、用建立的套接字与客户端通信(send/recv)。
第七步、关闭已经打开的套接字,跟文件处理是一样的。
五、实例演示
1、说明:
首先,程序运行在Windows下有环境依赖,要做跟Windows有关的初始化。包括一些头文件的包括,需要链接的库等。
其次是程序里面分别在客户端和服务端创建了一个线程用来收信息,增加程序的体验感。同样是windows的原因,调用createThird创建线程时要特别注意线程函数的格式。
也可以选用其他创建线程的函数。
2、客户端代码
#include<stdio.h> #include <stdlib.h> #include<string.h> #include <winsock2.h> #include <windows.h> /*添加库的方法:工程->设置->连接->对象/库模块 中加入ws2_32.lib*/ #pragma comment(lib,"ws2_32.lib") #define PORT 8888 #define ADDR "127.0.0.1" //函数声明 DWORD WINAPI ThreadProc(LPVOID lpParam); //主函数 int main(){ SOCKET scoket; SOCKADDR_IN serAddr; int i=0; char buffer[1024]; int nRet=0; WSADATA wsock; //第一步,很重要。做环境初始化 if(WSAStartup(MAKEWORD(2,2),&wsock)!=0) { return 0; } //第二步,获取套接字 if((scoket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))==INVALID_SOCKET) { WSACleanup();//获取套接字失败后,需要关闭已经初始化的环境 return 0; } //设置SOCKADDR_IN地址结构 serAddr.sin_family=AF_INET;//ipv4协议簇 serAddr.sin_port=htons(PORT);//设置端口 serAddr.sin_addr.s_addr=inet_addr(ADDR); //设置IP地址 //第三步,进行连接 if((connect(scoket,(SOCKADDR*)&serAddr,sizeof(serAddr)))==SOCKET_ERROR) { printf("error:%d",WSAGetLastError()); return 0; } //创建一个线程,用来接收数据。 //windows里面调用此函数,Linux不一定 CreateThread ( NULL, NULL, ThreadProc,&scoket,0, NULL); while(1) { memset(buffer,0,sizeof(buffer));//缓存清零 gets(buffer); if(strcmp("exit",buffer)==0) goto EXIT; if((nRet=send(scoket,buffer,strlen(buffer),0))==SOCKET_ERROR) { printf("error:%d",WSAGetLastError()); goto EXIT; } } //错误处理 EXIT: closesocket(scoket);/*关闭不使用的套接字,跟文件操作一样,网络也是稀缺资源。*/ WSACleanup(); //关闭已经初始化的环境 return 0; } //线程函数,注意返回值和参数类型是固定的 DWORD WINAPI ThreadProc(LPVOID lpParam) { SOCKET *sk=(SOCKET *)lpParam; char buffer[1024]; while(1) { memset(buffer,0,sizeof(buffer)); if(recv(*sk,buffer,sizeof(buffer),0)==SOCKET_ERROR) { printf("error:%d",WSAGetLastError()); closesocket(*sk); WSACleanup(); return 0; } if(strcmp(buffer,"exit")==0){ return 0; } puts(buffer); } }
3、服务端代码
#include<stdio.h> #include<string.h> #include <stdlib.h> #include <winsock2.h> #include<windows.h> /*添加库的方法:工程->设置->连接->对象/库模块 中加入ws2_32.lib*/ #pragma comment(lib,"WS2_32.lib") #define PORT 8888 #define ADDR "127.0.0.1" DWORD WINAPI ThreadProc(LPVOID lpParam); int main(int argc,char* argv[]){ WSADATA wsock; SOCKET listensocket,newconnection; SOCKADDR_IN serAddr,cliAddr; int cliAddrLen=sizeof(cliAddr); int nRet=0; char buffer[1024]; //第一步,很重要。做环境初始化 if(WSAStartup(MAKEWORD(2,2),&wsock)!=0) { return 0; } //第二步,获取套接字 if((listensocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET){ printf("error:%d",WSAGetLastError()); WSACleanup();//关闭已经初始化的环境 return 0; } printf("设置监听套接字\n"); //设置SOCKADDR_IN地址结构 serAddr.sin_family = AF_INET; serAddr.sin_port = htons(PORT); serAddr.sin_addr.s_addr=inet_addr(ADDR); //serAddr.sin_addr.S_un.S_addr = INADDR_ANY; printf("绑定\n"); //绑定套接字到相应的端口 if(bind(listensocket, (SOCKADDR *)&serAddr,sizeof(serAddr))== SOCKET_ERROR){ printf("error:%d",WSAGetLastError()); goto ERR; } printf("进入监听……\n"); //监听等待,无连接时处于阻塞状态 if(listen(listensocket, 5) == SOCKET_ERROR) { printf("error:%d",WSAGetLastError()); goto ERR; } //错误处理 goto NEXT; ERR: closesocket(listensocket); WSACleanup(); return 0; NEXT: printf("设置接收连接套接字\n"); //引用新的套接字与客户端进行通信 if((newconnection = accept(listensocket, (SOCKADDR *) &cliAddr, &cliAddrLen)) == INVALID_SOCKET){ printf("error:%d",WSAGetLastError()); goto ERR; } //关闭监听套接字。也可以循环监听,进行多并发处理。不关闭 closesocket(listensocket); printf("收发数据……"); //创建一个线程,用来接收数据。 //windows里面调用此函数,Linux不一定 CreateThread ( NULL, NULL, ThreadProc,&newconnection,0, NULL); //循环发送数据 while(1) { memset(buffer,0,sizeof(buffer));//清零 gets(buffer); if(strcmp(buffer,"exit")==0){ goto EXIT; } if((nRet=send(newconnection,buffer,strlen(buffer),0))==SOCKET_ERROR){ printf("error:%d",WSAGetLastError()); goto EXIT; } } EXIT: closesocket(newconnection); WSACleanup(); return 0; } //线程函数,注意返回值和参数类型是固定的 DWORD WINAPI ThreadProc(LPVOID lpParam) { SOCKET *sk=(SOCKET *)lpParam; char buffer[1024]; while(1) { memset(buffer,0,sizeof(buffer));//清零 if(recv(*sk,buffer,sizeof(buffer),0)==SOCKET_ERROR) { printf("error:%d",WSAGetLastError()); closesocket(*sk); WSACleanup(); return 0; } if(strcmp(buffer,"exit")==0){ return 0; } puts(buffer); } }
4、运行结果