TCP程序开发步骤

TCP是面向连接的协议,需要通信双方首先建立一个连接。因为TCP可靠、稳定的特点,它被应用于大部分场合,但它对系统资源要求比较高。

TCP协议服务端程序的开发流程如下所示。

// 初始化Winsock库,获得协议版本
// 创建服务Socket对象 (指定协议类型,地址族信息)
// 绑定Socket (将指定的IP,端口绑定给Socket)
// 开始监听,并且设置监听数量. (开始监听后,客户端就可以连接成功)
// 开启端口,接收连接
// 收发数据(利用建立连接的Socket对象进行通信)
// 关闭Socket连接
// 终止Winsock库的调用

开发一个TCP服务端程序,在完成初始化Winsock库创建套接字(Socket)对象两个通用步骤后,还要完成如下步骤。

(1)绑定套接字到指定IP地址和端口

无论是使用哪种协议的服务端程序,都要将服务端的IP地址和端口绑定给先前创建的套接字,客户端程序将与之进行通信。绑定套接字的函数是bind,原型如下。

int bind(
 SOCKET s, //套接字句柄
 const struct sockaddr FAR *name,    //要绑定的地址
 int namelen                         //name所指定的地址长度
);

第一个参数s是要绑定地址的套接字句柄,由socket函数返回。

第二个参数name是指向sockaddr结构体的指针,用来指定套接字所绑定的地址。

对于TCP、UDP协议,该地址通常是IP地址和端口号,对于TCP、UDP协议使用sockaddr_in结构体代替sockaddr。其定义如下。

struct sockaddr_in {
    short  sin_family;      //地址家族(地址格式),Windows为AF_INET
    u_short sin_port;       //端口号
    struct in_addr sin_addr; //IP地址
    char  sin_zero[8];     //占位值,通常为0
};

第一个成员sin_family同socket函数的af参数。

第二个成员sin_port指定了TCP或UDP通信服务的端口号。应用程序选择端口号时应该注意,如0~1023由IANA(Internet Assigned Numbers Authorith)管理,保留为公共服务使用,普通用户应用程序应该选择1024以上的端口号。同时需要注意,这里的值使用的是网络字节顺序,而计算机中存储的数字是主机字节顺序。Winsock库提供了一系列转换函数用于两种顺序之间的转换,如下所示。

u_short htons(u_short hostshort); //将u_short类型由主机字节顺序转换为网络字节顺序
u_long htonl(u_long hostlong);    //将u_long类型由主机字节顺序转换为网络字节顺序
u_short ntohs(u_short netshort);  //将u_short类型由网络字节顺序转换为主机字节顺序
u_long ntohl(u_long netlong);     //将u_long类型由网络字节顺序转换为主机字节顺序

例如我们要使用端口6000,那么应该是sin_port = htons(6000);

第三个成员sin_addr用来存储IP地址,它被定义为一个联合来处理整个32位的值,定义如下。

struct in_addr {
    union {
        struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
        struct { u_short s_w1,s_w2; } S_un_w;
        u_long S_addr;
    } S_un;

我们知道IP地址由四个部分组成,每个部分由点分开,通常是“xxx.xxx.xxx.xxx”格式。实际上IP地址是一个32位数据,即四个字节构成,每个字节对应由点分开的每个部分。所以每部分的最大值是255。如果直接使用字符串为此成员赋值,通常需要进行转换,Winsock库提供了如下转换函数。

unsigned long inet_addr(const char  FAR *cp);
char FAR * inet_ntoa(struct  in_addr in);

其中inet_addr函数的作用是将用字符串表示的IP地址准换为网络字节顺序的32位值。

inet_ntoa函数的作用是将用网络字节顺序32位值表示的IP地址转换为用字符串表示的IP地址。

第四个成员sin_zero是为了与sockaddr结构大小相同而设置的,没有其他含义。

函数执行成功则返回0,否则返回SOCKET_ERROR,使用WSAGetLastError函数获得错误代码。

(2)监听连接

当套接字完成绑定指定IP地址和端口号后就应该设置套接字进入监听状态,也就是监听客户端的连接。使用listen函数,原型如下。

int listen(
 SOCKET s,   //套接字句柄
 int backlog  //监听队列中允许保持尚未处理的最大连接数量
);

当服务端设置套接字进入监听状态,并且排队尚未满的情况下,客户端的连接函数就可以连接成功,否则客户端的连接函数将处理阻塞状态,直到连接成功或超时。

(3)接受连接请求

当服务端套接字进入监听状态后,接着应该使用accept函数接受排队等候的连接,如果有排队等候连接则立即返回完成连接,否则accept函数将进入阻塞状态,直到有连接到来,或者socket对象被关闭。accept函数原型如下。

SOCKET accept(
SOCKET s, //套接字句柄
struct sockaddr FAR *addr, //指向sockaddr_in结构的指针,用于接收对方的地址
int FAR *addrlen     //addr指针指向内存的大小
);

该函数在套接字s上取出未处理连接中的第一个连接,然后为这个连接创建新的套接字并返回其句柄,后期的数据的收发就是使用这个新套接字来完成的。

TCP客户端程序的开发流程如下所示。

// 初始化Winsock库,获得协议版本
// 创建服务Socket对象 (指定协议类型,地址族信息)
// 连接(向指定IP和端口的服务器进行连接)
// 收发数据(利用建立连接的Socket对象进行通信)
// 关闭Socket连接
// 终止对Winsock的调用

而对于TCP客户端程序的开发,在完成初始化Winsock库创建套接字(Socket)对象两个通用步骤后,接下来就是使用connect函数发起对服务端的连接,函数原型如下。

int connect(SOCKET s,            //套接字句柄
const struct sockaddr FAR *name, //指向sockaddr_in结构的指针,指定要连接的服务器的地址 
int namelen  //addr指针指向内存的大小
);

函数执行成功返回0,之后就可以利用套接字s进行收发数据。函数执行失败返回SOCKET_ERROR,使用WSAGetLastError函数进一步获得错误代码。

无论是TCP协议的服务端程序还是客户端程序,发送数据都是使用send函数,原型如下。

int send(
 SOCKET s,               //已建立连接的套接字句柄
 const char FAR *buf,    //要发送的内容所在内存首地址
 int len,                //发送内容的长度
 int flags               //指定调用方式,通常置为0
);

函数执行成功后返回实际发送数据的字节数。

接收数据使用recv函数,原型如下。

int recv(
 SOCKET s,          //已建立连接的套接字句柄
 char FAR *buf,     //要接收的内容所在内存首地址
 int len,           //接收数据缓冲区的长度
 int flags          //指定调用方式,通常置为0
);

函数执行成功后返回实际接收数据的字节数。在阻塞模式下,recv将阻塞线程的执行,直至接收到数据。

原文地址:https://www.cnblogs.com/cyx-b/p/12535263.html