首页 > 技术文章 > 【转/TCP协议编程】 基于TCP的Socket 编程

waimen 2017-02-14 09:47 原文

基于TCP(面向连接)的socket编程,分为客户端和服务器端。

客户端的流程如下:

(1)创建套接字(socket)

(2)向服务器发出连接请求(connect)

(3)和服务器端进行通信(send/recv)

(4)关闭套接字

服务器端的流程如下:

(1)创建套接字(socket)

(2)将套接字绑定到一个本地地址和端口上(bind)

(3)将套接字设为监听模式,准备接收客户端请求(listen)

(4)等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)

(5)用返回的套接字和客户端进行通信(send/recv)

(6)返回,等待另一个客户请求。

(7)关闭套接字。

 

下面通过一个具体例子讲解一下具体的过程和相关的函数。

客户端代码,运行于vs2008

// ClientTest.cpp : 定义控制台应用程序的入口点。  
//  
  
#include "stdafx.h"  
#include <stdio.h>  
#include <stdlib.h>  
#include <winsock2.h>  
  
#define SERVER_PORT 5208 //侦听端口  
  
  
int _tmain(int argc, _TCHAR* argv[])  
{  
    WORD wVersionRequested;  
    WSADATA wsaData;  
    int ret;  
    SOCKET sClient; //连接套接字  
    struct sockaddr_in saServer; //服务器地址信息  
    char *ptr;  
    BOOL fSuccess = TRUE;  
  
    //WinSock初始化  
    wVersionRequested = MAKEWORD(2, 2); //希望使用的WinSock DLL的版本  
    ret = WSAStartup(wVersionRequested, &wsaData);  //加载套接字库  
    if(ret!=0)  
    {  
        printf("WSAStartup() failed!\n");  
        //return 0;  
    }  
    //确认WinSock DLL支持版本2.2  
    if(LOBYTE(wsaData.wVersion)!=2 || HIBYTE(wsaData.wVersion)!=2)  
    {  
        WSACleanup();   //释放为该程序分配的资源,终止对winsock动态库的使用  
        printf("Invalid WinSock version!\n");  
        //return 0;  
    }  
  
    //创建Socket,使用TCP协议  
    sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
    if (sClient == INVALID_SOCKET)  
    {  
        WSACleanup();  
        printf("socket() failed!\n");  
        //return 0;  
    }  
  
    //构建服务器地址信息  
    saServer.sin_family = AF_INET; //地址家族  
    saServer.sin_port = htons(SERVER_PORT); //注意转化为网络节序  
    saServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  
  
    //连接服务器  
    ret = connect(sClient, (struct sockaddr *)&saServer, sizeof(saServer));  
    if (ret == SOCKET_ERROR)  
    {  
        printf("connect() failed!\n");  
        closesocket(sClient); //关闭套接字  
        WSACleanup();  
        //return 0;  
    }  
  
  
    char sendMessage[]="ZhongXingPengYue";   
    ret = send (sClient, (char *)&sendMessage, sizeof(sendMessage), 0);  
    if (ret == SOCKET_ERROR)  
    {  
        printf("send() failed!\n");  
    }  
    else  
        printf("client info has been sent!");  
    char recvBuf[100];  
    recv(sClient,recvBuf,100,0);  
    printf("%s\n",recvBuf);  
    closesocket(sClient); //关闭套接字  
    WSACleanup();  
    getchar();  
    //return 0;  
}  
  
<span style="font-size:16px;"></span>  

 

<span style="font-size:16px;"></span>   

第一步,加载套接字。使用WSAStartup 函数,如:ret = WSAStartup(wVersionRequested, &wsaData)。WSAStartup函数的原型为

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData) 

 

第一参数wVersionRequested,用来指定准备加载的winsock库的版本。利用MAKEWORD(x,y)宏来赋值。x是高位字节,表示副版本号;y是低位字节,表示主版本号。MAKEWORD(2, 2)表示版本号为2.2。

第二个参数是指向WSADATA结构的指针,是一个返回值,保存了库版本的有关信息。

 

       第二步,创建套接字。使用socket函数,如:sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)。socket函数的原型为:

SOCKET socket(int af, int type, int protocol );  

第一个参数,指定地址族,对于TCP/IP协议的套接字,只能为AF_INET;

第二个参数,指定socket类型,SOCK_STREAM指产生流式套接字,SOCK_DGRAM指产生数据报套接字,TCP/IP协议使用SOCK_STREAM。

第三个参数,与特定的地址家族相关的协议,TCP协议一般为IPPROTO_TCP。也可以写0,那么系统会根据地址格式和套接字类别,自动选择一个适合的协议。

如果socket创建成功,则返回一个新的SOCKET数据类型的套接字描述符;若失败,则返回INVALID_SOCKET,由此可以判断是否创建成功。

 

        第三步,连接服务器。使用connect函数,如:ret = connect(sClient, (struct sockaddr *)&saServer, sizeof(saServer))。connect函数函数原型为

int connect(SOCKET s, const struct sockaddr FAR* name, int namelen);  

第一个参数是将在上面建立连接的那个套接字的描述符,即之前创建socket的返回值sClient。

第二个参数是要连接的服务器端的地址信息。它是一个结构体类型struct sockaddr_in ,需要在调用connect函数之前构建服务器地址信息。sockaddr_in的定义如下:

struct sockaddr_in{  
  
short sin_family;  
  
unsigned short sin_port;  
  
struct in_addr sin_addr;  
  
char sin_zero[8]  
  
};  

设置服务器端口时,用到htons函数,该函数把一个u_short类型的值从主机字节顺序转换为TCP/IP网络字节顺序,因为不同的计算机存放多字节的顺序不同(基于Intel CPU是高字节存放在低地址,低字节存放在高地址),所以网络中不同主机间通信时,要统一采用网络字节顺序。设置服务器IP地址时,使用到inet_addr函数,它是将点分十进制的IP地址的字符串转换成unsigned long型。inet_ntoa函数做相反的转换。

第三个参数是服务器端地址结构体的大小。

 

        第四步,发送。使用send函数向服务器发送数据,如:ret = send (sClient, (char *)&sendMessage, sizeof(sendMessage), 0)。send函数的原型为

int send(SOCKET s, const char FAR* buf, int len, int flags);  

第一个参数,是一个与服务器已经建立连接的套接字。

第二个参数,指向包含要发送的数据的缓冲区的指针。

第三个参数,是所指向的缓冲区的长度。准确的说,应该是所要发送的数据的长度,因为不是缓冲区的所有数据都要同时发送。

第四个参数,它设定的值将影响函数的行为,一般将其设置为0即可。

如果发送失败,send会返回SOCKET_ERROR,由此可以判断发送是否成功。

 

        第五步,接收。使用recv函数接收服务器发过来的数据,如recv(sClient,recvBuf,100,0)。recv函数的原型为

int recv(SOCKET s, const char FAR* buf, int len, int flags);  

recv函数的参数的含义和send函数参数含义差不多,只是第二个参数是指向用来保存接收数据的缓冲区的指针。recv函数的返回值应该是所接收的数据的长度,如果返回SOCKET_ERROR表示接收失败;返回0表示服务器端关闭连接。

 

       第六步,关闭socket,释放资源。使用closesocket函数关闭套接字,如closesocket(sClient);使用WSACleanup函数释放为该程序分配的资源,终止对winsock动态库的使用,如WSACleanup();
 

服务器端代码,运行于vs2008

// ServerTest.cpp : 定义控制台应用程序的入口点。  
//  
  
#include "stdafx.h"  
#include <stdio.h>  
#include <winsock2.h>  
  
#define SERVER_PORT 5208 //侦听端口  
  
int _tmain(int argc, _TCHAR* argv[])  
{  
    WORD wVersionRequested;  
    WSADATA wsaData;  
    int ret, nLeft, length;  
    SOCKET sListen, sServer; //侦听套接字,连接套接字  
    struct sockaddr_in saServer, saClient; //地址信息     
    char *ptr;//用于遍历信息的指针     
    //WinSock初始化  
    wVersionRequested=MAKEWORD(2, 2); //希望使用的WinSock DLL 的版本  
    ret=WSAStartup(wVersionRequested, &wsaData);  
    if(ret!=0)  
    {  
        printf("WSAStartup() failed!\n");  
        //return 0;  
    }  
    //创建Socket,使用TCP协议  
    sListen=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
    if (sListen == INVALID_SOCKET)  
    {  
        WSACleanup();  
        printf("socket() faild!\n");  
        //return 0;  
    }  
    //构建本地地址信息  
    saServer.sin_family = AF_INET; //地址家族  
    saServer.sin_port = htons(SERVER_PORT); //注意转化为网络字节序  
    saServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //使用INADDR_ANY 指示任意地址  
    
    //绑定  
    ret = bind(sListen, (struct sockaddr *)&saServer, sizeof(saServer));  
    if (ret == SOCKET_ERROR)  
    {  
        printf("bind() faild! code:%d\n", WSAGetLastError());  
        closesocket(sListen); //关闭套接字  
        WSACleanup();  
        //return 0;  
    }  
    
    //侦听连接请求  
    ret = listen(sListen, 5);  
    if (ret == SOCKET_ERROR)  
    {  
        printf("listen() faild! code:%d\n", WSAGetLastError());  
        closesocket(sListen); //关闭套接字  
        //return 0;  
    }  
    
    printf("Waiting for client connecting!\n");  
    printf("Tips: Ctrl+c to quit!\n");  
    //阻塞等待接受客户端连接  
    while(1)//循环监听客户端,永远不停止,所以,在本项目中,我们没有心跳包。  
    {  
        length = sizeof(saClient);  
        sServer = accept(sListen, (struct sockaddr *)&saClient, &length);  
        if (sServer == INVALID_SOCKET)  
        {  
            printf("accept() faild! code:%d\n", WSAGetLastError());  
            closesocket(sListen); //关闭套接字  
            WSACleanup();  
            return 0;  
        }    
  
        char sendMessage[]="hello client";  //发送信息给客户端  
        send(sServer,sendMessage,strlen(sendMessage)+1,0);  
  
        char receiveMessage[5000];  
        nLeft = sizeof(receiveMessage);  
        ptr = (char *)&receiveMessage;  
        while(nLeft>0)  
        {  
            //接收数据  
            ret = recv(sServer, ptr, 5000, 0);  
            if (ret == SOCKET_ERROR)  
            {  
                printf("recv() failed!\n");  
                return 0;  
            }  
            if (ret == 0) //客户端已经关闭连接  
            {  
                printf("Client has closed the connection\n");  
                break;  
            }  
            nLeft -= ret;  
            ptr += ret;  
        }    
        printf("receive message:%s\n", receiveMessage);//打印我们接收到的消息。  
          
  
    }   
  //  closesocket(sListen);  
  //  closesocket(sServer);  
  //  WSACleanup();  
    return 0;  
}  

        第一步,加载套接字库,和客户端得加载套接字一样。

 

        第二步,创建监听套接字,sListen=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);仍然使用的是socket函数。

 

        第三步,绑定。使用bind函数,该函数的作用是将一个创建好的套接字绑定到本地的某个地址和端口上,该函数的原型为:

int bind(SOCKET s, const struct sockaddr FAR* name, int namelen);  

第一个参数,指定要绑定的套接字;

第二个参数,指定该套接字的地址信息,这里即服务器的地址信息,它仍是指向struct sockaddr_in类型的结构体的指针。这个结构体和客户端调用connect函数之前构建服务器地址信息的一样。其中INADDR_ANY 是指示任意地址,因为服务器含有可能多个网卡,可能有多个IP地址,这边指选择一个任意可用的地址。

第三个参数,地址的信息的长度。

 

        第四步,监听连接。使用listen函数,该函数是将指定的套接字设置为监听模式,如ret = listen(sListen, 5)。函数原型为

int listen(SOCKET s, int backlog);  

第一个参数,是要设置为监听的套接字描述符。

第二个参数,是等待连接队列的最大的长度。注意了,设置这个值是为了设置等待连接队列的最大长度,而不是在一个端口上可以连接的最大数目。例如,设置为5,当有6个连接请求同时到达,前面5个连接请求会被放到等待请求连接队列中,然后服务器依次处理这些请求服务,但是第6个连接请求会被拒绝。

 

       第五步,接受客户端的连接请求。使用accept函数接受客户端发送的连接请求,如sServer = accept(sListen, (struct sockaddr *)&saClient, &length);该函数的原型为

SOCKET accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);  

第一个参数,是一个已设为监听模式的socket的描述符。

第二个参数,是一个返回值,它指向一个struct sockaddr类型的结构体的变量,保存了发起连接的客户端得IP地址信息和端口信息。

第三个参数,也是一个返回值,指向整型的变量,保存了返回的地址信息的长度。

accept函数返回值是一个客户端和服务器连接的SOCKET类型的描述符,在服务器端标识着这个客户端。

      

       第六、七步是发送和接收,分别使用send和recv函数,这里和客户端的一样,不再重复。

       accept函数是放在一个死循环中的,一直监听客户的请求。当服务器关闭时要关闭所有的套接字,和释放资源。

推荐阅读