首页 > 编程笔记 > C语言笔记 阅读:153

C语言socket编程详解(附带实例)

套接字是网络通信的基本构件,最初是加利福尼亚大学伯克利分校为 UNIX 开发的网络通信编程接口。为了在 Windows 操作系统上使用套接字,20 世纪 90 年代初,微软和第三方厂商共同制定了一套标准,即 Windows Socket 规范,简称 WinSock。

所谓套接字,实际上是一个指向传输提供者的句柄。在 WinSock 中,就是通过操作该句柄来实现网络通信和管理的。

根据性质和作用的不同,套接字可以分为原始套接字、流式套接字和数据包套接字 3 种:

TCP套接字的socket编程

TCP 是面向连接的可靠的传输协议。利用 TCP 协议进行通信时,首先要建立通信双方的连接。一旦连接建立完成,就可以进行通信。TCP 提供了数据确认和数据重传的机制,保证了发送的数据一定能到达通信的对方。

基于 TCP,面向连接的 socket 编程的服务器端程序流程如下:
基于 TCP,面向连接的 socket 编程的客户端程序流程如下:
在服务器端,当调用 accept() 函数时,程序就会进行等待,直到有客户端调用 connect 函数发送连接请求,然后服务器接受该请求,这样服务器端与客户端就建立了连接,可以开始通信了。

注意,在服务器端要建立套接字绑定到指定的主机 IP 和端口上等待客户的请求,但是对于客户端来说,当发起连接请求并被接受后,在服务器端就保存了该客户端的IP地址和端口号的信息。对于服务器端来说,一旦建立连接,实际上它已经保存了客户端的IP地址和端口号的信息,因此可以利用返回的套接字进行与客户端的通信。

UDP套接字的socket编程

UDP 是无连接的不可靠的传输协议。采用 UDP 进行通信时,不需要建立连接,可以直接向一个 IP 地址发送数据,但是不能保证对方能收到。

对于基于 UDP,面向无连接的套接字编程来说,服务器端和客户端的概念不是特别的严格。可以把服务器称为接收端,客户端就是发送数据的发送端。

基于 UDP,面向无连接的 socket 编程的接收端程序流程如下:
基于 UDP,面向无连接的 socket 编程的发送端程序流程如下:
注意,基于 UDP 的套接字编程中,仍然需要使用 bind 进行绑定。虽然面向无连接的 socket 编程无须建立连接,但为了完成通信,首先应启动接收端,等待接收发送端发来的数据,这样接收端就必须告诉它自己的地址和端口。因此,必须调用 bind 函数将套接字绑定到一个本地地址和端口上。

基于 UDP 的套接字编程时,利用的是 sendto() 和 recvfrom() 两个函数实现数据的发送和接收;基于 TCP 的套接字编程时,发送数据使用的是 send() 函数,接收数据使用的是 recv() 函数。

套接字常用函数

前面介绍了使用套接字编写程序的流程,接下来介绍使用套接字编程时用到的函数。

1) WSAStartup()函数

WSAStartup() 函数的功能是初始化套接字库。其原型如下:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
注意,WSAStartup() 函数用于初始化 Ws2_32.dll 动态链接库。在使用其他套接字函数之前,必须先初始化 Ws2_32.dll 动态链接库。

typedef struct WSAData {
    WORD wVersion;
    WORD wHighVersion;
    char szDescription[WSADESCRIPTION_LEN+1];
    char szSystemStatus[WSASYS_STATUS_LEN+1];
    unsigned short iMaxSockets;
    unsigned short iMaxUdpDg;
    char FAR * lpVendorInfo;
} WSADATA, FAR * LPWSADATA;

例如,使用 WSAStartup 初始化套接字,版本号为 2.2:
WORD wVersionRequested;  /* WORD(字),类型为 unsigned short */
WSADATA wsaData;         /* 库版本信息结构 */
/* 定义版本类型,将两个字节组合成一个字,前面是低字节,后面是高字节 */
wVersionRequested = MAKEWORD(2, 2);  /* 表示版本号 */
/* 加载套接字库,初始化 Ws2_32.dll 动态链接库 */
WSAStartup(wVersionRequested, &wsaData);
从上面的代码中可以看出,MAKEWORD 宏的作用是:根据给定的两个无符号字节,创建一个 16 位的无符号整型,将创建的值赋予 wVersionRequested 变量,表示套接字的版本号。

2) socket函数

socket() 函数的功能是创建一个套接字。其原型如下:
SOCKET socket(int af,int type, int protocol);

例如,使用 socket() 函数创建一个套接字 socket_server:
/*创建服务器套接字*/
/*AF_INET表示指定地址族,SOCK_STREAM表示流式套接字TCP,特定的地址家族相关的协议*/
socket_server=socket(AF_INET,SOCK_STREAM,0);
在上面代码中,如果 socket() 函数调用成功,就会返回一个新的 SOCKET 数据类型的套接字描述符。使用定义好的套接字 socket_server 进行保存。

3) bind()函数

bind() 函数的功能是将套接字绑定到指定的端口和地址上。其原型如下:
int bind(SOCKET s,const struct sockaddr FAR*  name,int namelen);

在创建了套接字后,应该将该套接字绑定到本地的某个地址和端口上,这时就需要使用 bind() 函数了。例如,使用 bind() 函数绑定一个套接字:
SOCKADDR_IN Server_add;  /* 服务器地址信息结构 */
Server_add.sin_family = AF_INET;  /* 地址族,必须是 AF_INET,注意只有它不是网络字节顺序 */
Server_add.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  /* 主机地址 */
Server_add.sin_port = htons(5000);  /* 端口号 */
bind(socket_server, (SOCKADDR*)&Server_add, sizeof(SOCKADDR));  /* 使用 bind 函数进行绑定 */

4) listen()函数

listen() 函数的功能是将套接字设置为监听模式。对于流式套接字,必须处于监听模式才能够接收客户端套接字的连接。该函数的原型如下:
int listen(SOCKET s, int backlog);

例如,使用 listen() 函数设置套接字为监听状态:
listen(socket_server,5);
上述代码中,设置套接字为监听状态,为连接做准备,最大等待的数目为 5。

5) accept()函数

accept() 函数的功能是接受客户端的连接。在流式套接字中,只有当套接字处于监听状态时,才能接受客户端的连接。该函数的原型如下:
SOCKET accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);

例如,使用 accept() 函数接受客户端的连接请求,代码如下:
/*接受客户端的发送请求,等待客户端发送connect请求*/
socket_receive=accept(socket_server,(SOCKADDR*)&Client_add,&Length);
其中,socket_receive 用于保存接受请求后返回的新的套接字,socket_server 为绑定在地址和端口上的套接字,而 Client_add 是有关客户端的 IP 地址和端口的信息结构,最后的 Length 是 Client_add 的大小(可以使用 sizeof() 函数取得,然后用 Length 变量保存)。

6) closesocket()函数

closesocket() 函数的功能是关闭套接字。其原型如下:
int closesocket(SOCKET s);
其中,s 是套接字标识。如果参数 s 设置了 SO_DONTLINGER 选项,则调用该函数后会立即返回。此时如果有数据尚未传送完毕,则会继续传递数据,然后再关闭套接字。

例如,使用 closesocket() 函数关闭套接字,释放客户端的套接字资源,代码如下:
closesocket(socket_receive);  /* 释放客户端的套接字资源 */
在代码中,socket_receive 是一个套接字,当不使用时就可以利用 closesocket() 函数将其资源释放。

7) connect函数

connect() 函数的功能是发送一个连接请求。其原型如下:
int connect(SOCKET s,const struct sockaddr FAR*  name,int namelen);

例如,使用 connect() 函数与一个套接字建立连接,代码如下:
connect(socket_send,(SOCKADDR*)&Server_add,sizeof(SOCKADDR));
在上面代码中,socket_send 表示要与服务器建立连接的套接字,而 Server_add 是要连接的服务器地址信息。

8) htons() 函数

htons() 函数的功能是将一个 16 位的无符号短整型数据从主机排列方式转换为网络排列方式。其原型如下:
u_short htons(u_short hostshort);

例如,使用 htons() 函数对一个无符号短整型数据进行转换,代码如下:
Server_add.sin_port=htons(5000);
在上面代码中,Sever_add 是有关主机地址和端口的结构,其中 sin_port 表示的是端口号。因为端口号要使用网络排列方式,所以使用 htons() 函数进行转换,设定新的端口号。

9) htonl()函数

htonl() 函数的功能是将一个无符号长整型数据从主机排列方式转换为网络排列方式。其原型如下:
u_long htonl(u_long hostlong);

其使用方式与 htons() 函数相似,不过是将一个 32 位数值转换为 TCP/IP 网络字节顺序。

10) inet_addr函数

inet_addr() 函数的功能是将一个由点分十进制表示的 IP 地址字符串转换为 32 位的无符号长整型数据。其原型如下:
unsigned long inet_addr(const char FAR * cp);

例如,使用 inet_addr() 函数将一个以点分十进制格式表示的 IP 地址 192.168.1.43 转换为 32 位的无符号长整型数据,代码如下:
Server_add.sin_addr.S_un.S_addr = inet_addr("192.168.1.43");

11) recv() 函数

recv() 函数的功能是从面向连接的套接字中接收数据。其原型如下:
int recv(SOCKET s,char FAR* buf,int len,int flags);

例如,使用 recv() 函数接收数据,代码如下:
recv(socket_send,Receivebuf,100,0);
其中,socket_send 是用于连接的套接字,Receivebuf 是用来接收保存数据的空间,100 是该空间的大小。

12) send()函数

send() 函数用于在面向连接方式的套接字间发送数据。其原型如下:
int send(SOCKET s,const char FAR * buf, int len,int flags);

例如,使用 send() 函数发送数据,代码如下:
send(socket_receive,Sendbuf,100,0);
在上面代码中,socket_receive 用于连接的套接字,而 Sendbuf 保存要发送的数据,100 为该数据的大小。

13) recvfrom()函数

recvfrom() 函数用于接收一个数据报信息并保存源地址。其原型如下:
int recvfrom(SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen);

14) sendto()函数

sendto() 函数用于向一个特定的目的方发送数据。其原型如下:
int sendto(SOCKET s,const char FAR * buf,int len,int flags,const struct sockaddr FAR * to,int tolen);

15) WSACleanup()函数

WSACleanup() 函数用于释放为 Ws2_32.dll 动态链接库初始化时分配的资源。其原型如下:
int WSACleanup(void);
例如,使用该函数可关闭动态链接库,代码如下:
WSACleanup(); /*关闭动态链接库*/

基于TCP的网络聊天程序

接下来将编写一个基于 TCP 网络通信的聊天程序,希望通过本案例,读者可对前面学习的内容有一个更深的理解。
#include<stdio.h>
#include<winsock.h>  /* 包含 winsock 头文件 */

int main()
{
    /*--------------------------定义变量---------------------------*/
    char Sendbuf[100];  /* 发送数据的缓冲区 */
    char Receivebuf[100];  /* 接收数据的缓冲区 */
    int SendLen;  /* 发送数据的长度 */
    int ReceiveLen;  /* 接收数据的长度 */
    int Length;  /* SOCKADDR 的大小 */

    SOCKET socket_server;  /* 定义服务器套接字 */
    SOCKET socket_receive;  /* 定义连接套接字 */

    SOCKADDR_IN Server_add;  /* 服务器地址信息结构 */
    SOCKADDR_IN Client_add;  /* 客户端地址信息结构 */

    WORD wVersionRequested;  /* 字(word):unsigned short */
    WSADATA wsaData;  /* 库版本信息结构 */
    int error;

    /*--------------------------初始化套接字---------------------------*/
    /* 定义版本类型,将两个字节组合成一个字,前面是低字节,后面是高字节 */
    wVersionRequested = MAKEWORD(2, 2);  /* 表示版本号 */
    /* 加载套接字库,初始化 Ws2_32.dll 动态链接库 */
    error = WSAStartup(wVersionRequested, &wsaData);
    if(error!=0)
    {
        printf("加载套接字失败!");
        return 0;  /* 程序结束 */
    }
    /* 判断请求加载的版本号是否符合要求 */
    if((LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) )
    {
        WSACleanup();  /* 不符合,关闭套接字库 */
        return 0;  /* 程序结束 */
    }

    /*--------------------------设置连接地址---------------------------*/
    /*-------------------------------------------*/
    Server_add.sin_family = AF_INET;  /* 地址族,必须是 AF_INET。注意,只有它不是网络字节顺序 */
    Server_add.sin_addr.S_un.S_addr = htonl(INADDR_ANY); /* 主机地址 */
    Server_add.sin_port = htons(5000);  /* 端口号 */

    /*--------------------------创建套接字---------------------------*/
    /* AF_INET 表示指定地址族,SOCK_STREAM 表示该套接字 TCP,特定的地址族相关协议 */
    socket_server = socket(AF_INET, SOCK_STREAM, 0);

    /*--------------------------绑定套接字到本地的某个地址和端口上------------*/
    /*-------------------------------------------*/
    /* socket_server 为套接字,(SOCKADDR*)&Server_add 为服务器地址 */
    if(bind(socket_server, (SOCKADDR*)&Server_add, sizeof(SOCKADDR)) == SOCKET_ERROR)
    {
        printf("绑定失败\n");
    }

    /*--------------------------设置套接字为监听状态---------------------------*/
    /* 监听状态,为连接做准备,最大等待数目为 5 */
    if(listen(socket_server, 5) < 0)
    {
        printf("监听失败\n");
    }

    /*--------------------------接受连接-------------------------- */
    Length = sizeof(SOCKADDR);  /* 接受客户端的发送请求,等待客户端发送 connect 请求 */
    socket_receive = accept(socket_server, (SOCKADDR*)&Client_add, &Length);
    if(socket_receive == SOCKET_ERROR)
    {
        printf("接受连接失败\n");
    }

    /*--------------------------进行聊天-------------------------- */
    /*------------------------------------------ */
    while(1)  /* 无限循环 */
    {
        /*--------------------------接收数据-------------------------- */
        ReceiveLen = recv(socket_receive, Receivebuf, 100, 0);
        if(ReceiveLen < 0)
        {
            printf("接收失败\n");
            printf("程序退出\n");
            break;
        }
        else
        {
            printf("client say: %s\n", Receivebuf);
        }

        /*--------------------------发送数据-------------------------- */
        printf("please enter message:");
        scanf("%s", Sendbuf);
        SendLen = send(socket_receive, Sendbuf, 100, 0);
        if(SendLen < 0)
        {
            printf("发送失败\n");
        }
    }

    /*--------------------------释放套接字,关闭动态库-------------------------- */
    closesocket(socket_receive);  /* 释放客户端的套接字资源 */
    closesocket(socket_server);  /* 释放服务器的套接字资源 */
    WSACleanup();  /* 关闭动态链接库 */
    return 0;
}
运行程序之前,要先添加相应的库文件 ws2_32.lib。以上就是网络聊天程序服务器端的代码。整个程序流程按照以下顺序编写:
根据有关 TCP 套接字 socket 编程中的客户端设计过程,编写下面的代码:
#include<stdio.h>
#include<winsock.h>  /* 包含 winsock 头文件 */

int main()
{
    /*--------------------------定义变量---------------------------*/
    char Sendbuf[100];  /* 发送数据的缓冲区 */
    char Receivebuf[100];  /* 接收数据的缓冲区 */
    int SendLen;  /* 发送数据的长度 */
    int ReceiveLen;  /* 接收数据的长度 */

    SOCKET socket_send;  /* 定义套接字 */
    SOCKADDR_IN Server_add;  /* 服务器地址信息结构 */

    WORD wVersionRequested;  /* 字(word):unsigned short */
    WSADATA wsaData;  /* 库版本信息结构 */
    int error;

    /*--------------------------初始化套接字---------------------------*/
    /* 加载套接字库,初始化 Ws2_32.dll 动态链接库 */
    error = WSAStartup(wVersionRequested, &wsaData);
    if(error!=0)
    {
        printf("加载套接字失败!");
        return 0;  /* 程序结束 */
    }
    /* 判断请求加载的版本号是否符合要求 */
    if((LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) )
    {
        WSACleanup();  /* 不符合,关闭套接字库 */
        return 0;  /* 程序结束 */
    }

    /*--------------------------设置服务器地址---------------------------*/
    Server_add.sin_family = AF_INET;  /* 地址族,必须是 AF_INET。注意,只有它不是网络字节顺序 */
    Server_add.sin_addr.S_un.S_addr = inet_addr("192.168.1.43");  /* 服务器地址,将一个点分十进制表示为 IP 地址,inet_ntoa 是将地址转换成字符串 */
    Server_add.sin_port = htons(5000);  /* 端口号 */

    /*--------------------------进行服务器连接---------------------------*/
    /* 客户端创建套接字,但是不需要绑定,只需要和服务器建立起连接即可 */
    /* socket_send 表示的是套接字,Server_add 是服务器的地址结构 */
    socket_send = socket(AF_INET, SOCK_STREAM, 0);

    /*--------------------------创建用于连接的套接字---------------------------*/
    /* AF_INET 表示指定地址族,SOCK_STREAM 表示流式套接字 TCP,特定的地址族相关协议 */
    if(connect(socket_send, (SOCKADDR*)&Server_add, sizeof(SOCKADDR)) == SOCKET_ERROR)
    {
        printf("连接失败\n");
    }

    /*--------------------------进行聊天-------------------------- */
    while(1)  /* 无限循环 */
    {
        /*--------------------------发送数据过程-------------------------- */
        printf("please enter message:");
        scanf("%s", Sendbuf);
        SendLen = send(socket_send, Sendbuf, 100, 0);  /* 发送数据 */
        if(SendLen < 0)
        {
            printf("发送失败\n");
        }

        /*--------------------------接收数据过程-------------------------- */
        ReceiveLen = recv(socket_send, Receivebuf, 100, 0);  /* 接收数据 */
        if(ReceiveLen < 0)
        {
            printf("接收失败\n");
            printf("程序退出\n");
            break;  /* 跳出循环 */
        }
        else
        {
            printf("Server say: %s\n", Receivebuf);
        }
    }

    /*--------------------------释放套接字,关闭动态库-------------------------- */
    closesocket(socket_send);  /* 释放套接字资源 */
    WSACleanup();  /* 关闭动态链接库 */
    return 0;
}
以上就是网络聊天程序客户端的代码。整个程序流程按照以下顺序编写:
先运行第一个程序,然后运行第二个程序。首先在客户端输入数据,按 Enter 键后,即可以在服务器端看到输入的信息。客户端输入完后,服务器端就可以对其进行回复。在服务器端输入数据并按 Enter 键,可发送消息到客户端。

客户端程序运行效果为:

please enter message:Hello!
Server say: Hello~
please enter message:


服务器端程序运行效果为:

client say: Hello!
please enter message:Hello~


注意,要实现网络通信,一定先运行服务器端,再运行客户端。注意,在客户端输入 IP 地址时,计划与哪台计算机通信,就输入哪台计算机的 IP 地址。

相关文章