C/C++SOCKET网络编程实例及编程思路

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zgcr654321/article/details/82106746

设计一个TCP服务器/客户端,从客户端获得一个字符串,服务器把字符串全部转为大写字母之后返回给客户端。

设计思路:

函数介绍:

socket编程要调用各种socket函数,需要库Ws2_32.lib和头文件Winsock2.h。

#include <WinSock2.h>

//在windows中,我们一般使用WinSock2头文件中的函数来进行网络通信。

#pragma comment(lib, "ws2_32.lib")

//静态加入一个lib文件,也就是库文件。ws2_32.lib文件,提供了对socket网络API的支持,用到winsock2.h中的API时要用到ws3_32.lib文件。

WSAStartup( )函数:

wsastartup主要就是进行相应的socket库绑定。当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。

该函数执行成功后返回0。 

应用程序在完成对请求的Socket库的使用后,要调用WSACleanup函数来解除与Socket库的绑定并且释放Socket库所占用的系统资源。 

Windows下,Socket是以DLL的形式实现的。在DLL内部维持着一个计数器,只有第一次调用WSAStartup才真正装载DLL,以后的 调用只是简单的增加计数器,而WSACleanup函数的功能则刚好相反,每调用一次使计数器减1,当计数器减到0时,DLL就从内存中被卸载。

函数原型:int PASCAL FAR WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

wVersionRequested是Windows Sockets API提供的调用方可使用的最高版本号。lpWSAData 是指向WSADATA结构体的指针,用来接收WSAStartup函数调用后返回的Windows Sockets数据。

如:

WSADATA wsadata;

WSAStartup(MAKEWORD(2, 2), &wsadata);

//MAKEWORD(2, 2)即使用2.2版本的Windows Sockets API,&wsadata为一个指向WSADATA型的名为wsadata的变量的指针

WSADATA结构体:

struct WSADATA {

  WORD wVersion;//Windows Sockets DLL期望调用者使用的Windows Sockets规范的版本。

  WORD wHighVersion;//这个DLL能够支持的Windows Sockets规范的最高版本。

  char szDescription[WSADESCRIPTION_LEN+1];

//以null结尾的ASCII字符串,Windows Sockets DLL将对Windows Sockets实现的描述拷贝到这个字符串中,包括制造商标识。

  char szSystemStatus[WSASYSSTATUS_LEN+1];

//以null结尾的ASCII字符串,Windows Sockets DLL把有关的状态或配置信息拷贝到该字符串中。

  unsigned short iMaxSockets;//单个进程能打开的socktet最大数目。

  unsigned short iMaxUdpDg;

//iMaxUdpDg Windows Sockets应用程序能够发送或接收的最大的用户数据包协议(UDP)的数据包大小,以字节为单位。

  char *lpVendorInfo;

//lpVendorInfo 指向销售商的数据结构的指针。这个结构的定义(如果有)超出了WindowsSockets规范的范围。WinSock2.0版中已被废弃。

};

WSADATA结构被用来保存函数WSAStartup返回的Windows Sockets初始化信息。

socket()函数:

函数原型:int socket(int domine,int type,int protocol);

int domin为协议族。协议族决定了socket的地址类型,在通信中必须采用对应的地址。int type是套接口类型,主要SOCK_STREAM(建立TCP连接)、SOCK_DGRAM(建立UDP)、SOCK_RAW(该接口允许对较低层次协议,如IP,ICMP直接访问)。int protocol:指定协议。通常情况设为0,为0时,会自动选择type类型对应的默认协议。

socket函数成功则返回套接字描述符(套接字的索引),失败则返回-1。

如:

socket(AF_INET, SOCK_STREAM, 0);

//三个参数分别是:协议族,AF_INET表示ipv4。套接口类型,SOCK_STREAM表示建立TCP连接。指定协议设为0时,会自动选择套接口类型对应的默认协议。

SOCKADDR_IN结构体:

struct sockaddr_in { 
   short int sin_family; /* 地址族 */ 
   unsigned short int sin_port; /* 端口号 */ 
   struct in_addr sin_addr; /* IP地址 */ 
   unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小 */ 
};

sin_zero(它用来将sockaddr_in结构填充到与struct sockaddr同样的长度)应该用bzero()或memset()函数将其置为零。指向sockaddr_in 的指针和指向sockaddr的指针可以相互转换,这意味着如果一个函数所需参数类型是sockaddr时,你可以在函数调用的时候将一个指向 sockaddr_in的指针转换为指向sockaddr的指针;或者相反。sin_family通常被赋AF_INET;sin_port和 sin_addr应该转换成为网络字节优先顺序;而sin_addr则不需要转换。  

如:

SOCKADDR_IN addrSrv;//定义一个SOCKADDR_IN类型变量addsrv,表示服务器端的套接字描述符
addrSrv.sin_family = AF_INET;//地址族赋值,AF_INET表示IPv4
addrSrv.sin_port = htons(port); //端口号赋值,1024以上的端口号
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//ip地址赋值,INADDR_ANY表示填入本机ip地址
//S表示Sock,un表示union联合,S_un.S_addr表示SOCKADDR_IN结构中的in_addr联合体结构中的S_addr成员。

字节顺序转换函数:

16位的转换函数:ntohs和htons
  ntohs(network to host short)是将网络字节顺序转换为主机字节顺序,返回值是一个16位的整数,即2个字节长度的整数(1字节=8位)short int,也可以写作uint16_t。
  htons(host to network short)是将主机字节顺序转换为网络字节顺序,返回值也是一个16位的整数short int。

32位转换函数ntohl和htonl。
  ntohl(network to host long)是将网络字节顺序转换为主机字节顺序,返回值是一个16位的整数,即2个字节长度的整数(1字节=8位)long int,也可以写作uint32_t。
  htonl(host to network long)是将主机字节顺序转换为网络字节顺序,返回值也是一个16位的整数long int。

bind()函数:

函数原型:int bind(int socket, const struct sockaddr*address,socklen_t address_len);

bind()函数将一个地址分配给一个未命名的套接字。使用socket()函数创建的那些套接字初始化是没有命名的,它们只有通过地址族才能被识别。

socket参数:指定了需要绑定的套接字的文件描述符。Address参数:指向一个sockaddr结构体,这个结构体中包含着要绑定到套接字的地址。地址的长度和格式依赖于套接字支持的地址族。address_len参数:指定了sockaddr结构体的长度。参数address指向了这个sockaddr结构体。 

一旦bind()函数成功执行,函数会返回0.否则的话就返回-1,而且errno也被设置用来解释是什么错误。

如:

bind(sockSrv, (LPSOCKADDR) & addrSrv, sizeof(SOCKADDR_IN));
//sockSrv为套接字描述符。
//&addrSrv指向一个sockaddr结构体,这个结构体中包含着要绑定到套接字的地址。
//sizeof(SOCKADDR_IN)指定了sockaddr结构体的长度。

listen()函数:

listen在套接字函数中表示让一个套接字处于监听到来的连接请求的状态。执行listen 之后套接字进入被动模式。

(2) 队列满了以后,将拒绝新的连接请求。客户端将出现连接D 错误WSAECONNREFUSED。

(3) 在正在listen的套接字上执行listen不起作用。

函数原型:int listen(SOCKET sockfd, int backlog);

sockfd是一个已绑定未被连接的套接字描述符,backlog为连接请求队列的最大长度。

函数运行无错误,返回0,否则,返回SOCKET ERROR,可以调用函数WSAGetLastError取得错误代码。

如:

listen(sockSrv, 10)。

//sockSrv为一个已绑定未被连接的套接字描述符,10为连接请求队列的最大长度。

accept()函数:

accept()系统调用主要用在基于连接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET。它提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。新建立的套接字准备发送send()和接收数据recv()。函数运行成功时,返回非负整数,该整数是接收到套接字的描述符;出错时,返回-1,相应地设定全局变量errno。

函数原型:int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

sockfd是利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;addr是指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写,返回地址addr的确切格式由套接字的地址类别(比如TCP或UDP)决定;若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;addrlen是一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值。addrlen是个局部整形变量,设置为sizeof(struct   sockaddr_in)。

如:

accept(sockSrv, (SOCKADDR * ) & addrClient, &len);

//sockSrv是利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接。
//&addrClient为指向客户机的sockaddr结构的指针。
//&len,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值。

注意:

一般来说,实现时accept()为阻塞函数,当监听socket调用accept()时,它先到自己的receive_buf中查看是否有连接数据包;若有,把数据拷贝出来,删掉接收到的数据包,创建新的socket与客户发来的地址建立连接;若没有,就阻塞等待。

inet_ntoa() 函数:

函数声明:char *inet_ntoa (struct in_addr);

将网络地址转换成“.”点隔的字符串格式,即点分十进制的ip地址,并返回指向ip地址字符串在静态内存中的指针。

send()函数:

函数原型:int send( SOCKET s, const char FAR *buf, int len, int flags );  

不论是客户端还是服务器端应用程序都用send函数来向TCP连接的另一端发送数据。客户端程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。

该函数的第一个参数指定发送端套接字描述符;第二个参数指明一个存放应用程序要发送数据的缓冲区;第三个参数指明实际要发送的数据的字节数;第四个参数一般置0。

Send函数的返回值有三类:返回值=0;返回值<0:发送失败,错误原因存于全局变量errno中;返回值>0:表示发送的字节数(实际上是拷贝到发送缓冲中的字节数)。

如:

send(sockConn, buff, sizeof(buff), 0);

recv()函数:

函数原型:int recv( SOCKET s,     char FAR *buf,      int len,     int flags     );   

不论是客户端还是服务器端应用程序都用recv函数从TCP连接的另一端接收数据。

该函数的第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;第三个参数指明buf的长度;第四个参数一般置0。

在同步Socket的执行流程中,当应用程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到 协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

阻塞与非阻塞时recv返回值没有区别,都是:<0 出错;=0 对方调用了close API来关闭连接;>0 接收到的数据大小。

如:

recv(sockConn, recvBuf, sizeof(recvBuf), 0); 

closesocket()函数: 

函数原型:int   closesocket(SOCKET s); 

closesocket函数用来关闭一个描述符为s的套接字。由于每个进程中都有一个套接字描述符表,表中的每个套接字描述符都对应了一个位于操作系统缓冲区中的套接字数据结构,因此有可能有几个套接字描述符指向同一个套接字数据结构。套接字数据结构中专门有一个字段存放该结构的被引用次数,即有多少个套接字描述符指向该结构。当调用closesocket函数时,操作系统先检查套接字数据结构中的该字段的值,如果为1,就表明只有一个套接字描述符指向它,因此操作系统就先把s在套接字描述符表中对应的那条表项清除,并且释放s对应的套接字数据结构;如果该字段大于1,那么操作系统仅仅清除s在套接字描述符表中的对应表项,并且把s对应的套接字数据结构的引用次数减1。 

closesocket函数如果执行成功就返回0,否则返回SOCKET_ERROR。 

如:

closesocket(sockConn);

connect()函数: 

函数原型:int connect(SOCKET   s,const   struct   sockaddr   FAR   *name,int   namelen); 

客户程序调用connect函数来使客户Socket   s与监听于name所指定的计算机的特定端口上的服务Socket进行连接。如果连接成功,connect返回0;如果失败则返回SOCKET_ERROR。

如:

connect(sockClient, (struct sockaddr *) &addrSrv, sizeof(addrSrv));

//sockSrv为套接字描述符,指向一个sockaddr结构体,这个结构体中包含着要绑定到套接字的地址。
//&addrSrv指向一个sockaddr结构体,这个结构体中包含着要绑定到套接字的地址。
//sizeof(SOCKADDR_IN)指定了sockaddr结构体的长度。

实现的代码简述:

实现服务器端和客户机端可连续地发送和接收(发一次接收一次),服务器端先发,客户机端后发,并且能将对方发送的字符串中小写字母改写为大写字母再发回给对方。客户机端识别最后一个输入字符为#时退出程序,服务器端识别最后一个输入字符为#时断开与当前客户机端连接,并可选择输入Y或N来选择是否继续运行服务器端程序。

服务器端代码:

#include <WinSock2.h>//在windows中我们一般使用WinSock2头文件中的函数来进行网络通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
    printf("这是服务器端程序!\n");
    WSADATA wsadata;//WSADATA数据类型用来存储被WSAStartup函数调用后返回的Windows Sockets数据。
    int port=8000;//设置服务器端口号
    char buff[1024];//发送缓冲区
    char recvBuf[1024];//接收缓冲区
    printf("服务器端口号设为8000\n");
    WSAStartup(MAKEWORD(2, 2), &wsadata);
    SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);//创建用于监听的套接字
    SOCKADDR_IN addrSrv;//定义一个SOCKADDR_IN类型变量addsrv,表示服务器端的套接字描述符
    addrSrv.sin_family = AF_INET;//地址族赋值,AF_INET表示IPv4
    addrSrv.sin_port = htons(port); //端口号赋值,1024以上的端口号
    addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//ip地址赋值,INADDR_ANY表示填入本机ip地址
    bind(sockSrv, (LPSOCKADDR) & addrSrv, sizeof(SOCKADDR_IN));
    listen(sockSrv, 10);
    printf("服务器端准备完成,开始等待客户机连接请求:\n");
    while(1){
        SOCKADDR_IN addrClient;//定义一个SOCKADDR_IN类型变量Client,表示客户机端的套接字描述符
        int len = sizeof(SOCKADDR);//取得SOCKADDR_IN结构体的长度
        SOCKET sockConn = accept(sockSrv, (SOCKADDR * ) & addrClient, &len);//sockConn接收连接请求的套接字描述符
        printf("与客户机端连接成功,连接的客户机端IP为:%s\n", inet_ntoa(addrClient.sin_addr));//打印客户机ip地址
        while (1) {//等待客户请求到来
            printf("请输入要发送给客户机端的信息(字符串),不大于1024个字符,回车停止输入,当最后一个字符为#时关闭与客户端连接:\n");//开始发送数据
            gets(buff);
            fflush(stdin);
            int temp=strlen(buff);
            if(buff[temp-1]=='#')
                break;
            send(sockConn, buff, sizeof(buff), 0);
            printf("发送给客户机端的信息发送完成\n");
            memset(recvBuf, 0, sizeof(recvBuf));//初始化接收服务器端的缓冲区
            recv(sockConn, recvBuf, sizeof(recvBuf), 0);  //接收数据
            printf("接收到客户机端发来的信息:\n");//打印接收到的数据
            printf("%s\n",recvBuf);//打印接收到的数据
            printf("将接收到的信息由小写字母改成大写字母,并将改写后的信息发给客户机端\n");
            int length=strlen(recvBuf);
            memset(buff, 0, sizeof(buff));
            for(int i=0;i<length;i++){
                buff[i]=recvBuf[i];
                if(recvBuf[i]>=97&&recvBuf[i]<=122)
                    buff[i]-=32;
            }
            send(sockConn, buff, sizeof(buff), 0);
            printf("改写后的信息发送完成\n");
            memset(recvBuf, 0, sizeof(recvBuf));//初始化接收服务器端的缓冲区
            printf("接收到客户机端发来的改写后的信息:\n");//打印接收到的数据
            recv(sockConn, recvBuf, sizeof(recvBuf), 0);//接收数据
            printf("%s\n",recvBuf);//打印接收到的数据
        }
        printf("关闭套接字\n");//打印接收到的数据
        closesocket(sockConn);// closesocket函数用来关闭一个描述符为sockConn的套接字
        int flag;
        printf("是否还想运行服务器端程序?输入Y或N\n");
        scanf("%c",&flag);
        if(flag=='N')
            break;
    }
    closesocket(sockSrv);
    WSACleanup();//操作成功返回值为0;否则返回值为SOCKET_ERROR,可以通过调用WSAGetLastError获取错误代码。
    system("pause");//system("pause")就是从程序里调用“pause”命令.
    return 0;
}

客户机端代码:

#include <WinSock2.h>//在windows中我们一般使用WinSock2头文件中的函数来进行网络通信
#include <stdio.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
    printf("这是客户机端程序!\n");
    WSADATA wsaData;//WSADATA数据类型用来存储被WSAStartup函数调用后返回的Windows Sockets数据。
    char buff[1024];//发送缓冲区
    char recvBuf[1024];//接收缓冲区
    memset(buff, 0, sizeof(buff));//初始化缓冲区值为0
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    SOCKADDR_IN addrSrv;//定义一个SOCKADDR_IN类型变量addsrv,表示服务器端的套接字描述符
    addrSrv.sin_family = AF_INET;//地址族赋值,AF_INET表示IPv4
    addrSrv.sin_port = htons(8000);//端口号赋值,1024以上的端口号
    addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//ip地址赋值,INADDR_ANY表示填入本机ip地址
    SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);//创建客户机端套接字
    connect(sockClient, (struct sockaddr *) &addrSrv, sizeof(addrSrv));
    printf("客户机端已连接服务器!\n");
    while(1){
        memset(recvBuf, 0, sizeof(recvBuf));
        printf("接收服务器端发来的信息:\n");
        recv(sockClient, recvBuf, sizeof(recvBuf), 0);//接收数据
        printf("%s\n", recvBuf);//打印接收的数据
        printf("请输入要发送给服务器端的信息(字符串),不大于1024个字符,回车停止输入,当最后一个字符为#时关闭客户机:\n");//发送数据
        gets(buff);
        fflush(stdin);
        int temp=strlen(buff);
        if(buff[temp-1]=='#')
            break;
        send(sockClient, buff, sizeof(buff), 0);
        printf("发送给服务器端的信息发送完成\n");
        printf("将接收到的信息由小写字母改成大写字母,并将改写后的信息发给服务器端\n");
        int length=strlen(recvBuf);
        memset(buff, 0, sizeof(buff));
        for(int i=0;i<length;i++){
            buff[i]=recvBuf[i];
            if(recvBuf[i]>=97&&recvBuf[i]<=122)
                buff[i]-=32;
        }
        send(sockClient, buff, sizeof(buff), 0);
        printf("改写后的信息发送完成\n");
        memset(recvBuf, 0, sizeof(recvBuf));
        printf("接收到服务器端发来的改写后的信息:\n");
        recv(sockClient, recvBuf, sizeof(recvBuf), 0);
        printf("%s\n", recvBuf);//打印接收的数据
    }
    printf("关闭套接字\n");
    closesocket(sockClient);//关闭客户机套接字
    WSACleanup();//操作成功返回值为0;否则返回值为SOCKET_ERROR,可以通过调用WSAGetLastError获取错误代码。
    system("pause");//system("pause")就是从程序里调用“pause”命令.
    return 0;
}

注意:

编译时请先在编译软件中的链接设置中添加wsock32库,以codeblocks为例:

settings->complier->linksettings->add

添加wsock32链接库。

如图所示:

添加完成后即可正常编译。

运行时请先打开server.cpp和client.cpp所在的文件目录,按住shift,点击鼠标右键,选择在此处打开命令窗口,先运行服务器端,即输入server.exe,然后运行客户机端,即输入client.exe。

运行结果截图:

猜你喜欢

转载自blog.csdn.net/zgcr654321/article/details/82106746