一、基础概念

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、运行结果