摘要
在学习服务器的过程中需要使用到winsock以及Socket编程,这里记录一下学到的Socket基础
Socket套接字
Socket就是一种通过(IP + Port)并遵循TCP/UDP等协议的进程间通信的技术,类似于UNIX中的管道,详细的教程度娘上很多。
Socket主要有两类:
(1) 标准Socket,又称为Berkeley Socket,主要用于Linux/UNIX
(2) Winsock,主要用于Win平台Socket函数
以Winsock为例,有一系列基本的Socket函数用来实现Socket通信
(1) Socket // Socket 函数用于创建一个Socket套接字,函数原型如下 SOCKET socket( int af, //使用的协议族 int type, //Socket类型 int protocol //使用的协议地址 ); // Socket 协议族在计算机里表示为一个整数,可以取值AF_INET // Socket 类型有两种:SOCK_STREAM 和 SOCK_DGRAM,代表流Socket(Tcp)和数据报Socket(UDP) // 如果函数成功,返回一个Socket套接字,否则,返回INVALID_SOCKET,在建立Socket连接之前,必须先创建所需的Socket套接字 SOCKET s; s = socket(AF_INET, SOCK_STREAM, 0);
在QT使用socket函数的时候出现’undefined reference to `_imp__socket’ 错误,解决办法是在PRO文件中加入如下语句:
CONFIG += c++11 // 以防万一 LIBS += -lpthread libwsock32 libws2_32
而且在头文件中需要引入
<windows.h>
或者<winsock2.h>
让我们接着Socket往下说
(2) Connect // Connect函数用于尝试与远端建立一个Socket连接,函数原型如下: int connect( SOCKET s, //Socket描述字 const struct sockaddr* name, //远端的地址 int namelen //远端地址的长度 ); // 在进行连接时,远端地址是一个SOCKADDR的结构,定义为: struct sockaddr_in { short sin_family, //Socket组 u_short sin_port, // 端口 struct in_addr sin_addr, //IP地址 char sin_zero[8] //结构的长度 } // 如果连接成功,返回0,否则返回SOCKET_ERROR。 // 对于非阻塞模式的Socket连接,返回结果通常都是SOCKET_ERROR, // 并且错误代码为WSAEWOULDBLOCK,表示连接正在进行,而不是一个真正的错误 //建立连接通常都是由客户端发出连接请求 SOCKET s; SOCKADDR_IN ServerAddr; ServerAddr.sin_falimy = AF_INET; ServerAddr.sin_port = htons(80); //也可以自定义其他Port ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); connect(s, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
在QT使用connect函数的时候,会与QT本身的connect函数冲突,解决办法是加::域分隔符,如下:
::connect(s,(SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
让我们接着Connect往下说
(3) Send // Send函数用于在某个Socket上向远端发送数据,原型为: int send( SOCKET s, const char* buf, //存放发送数据的缓冲区 int len, //将要发送的数据长度 int flags //发送时使用的附加参数 ) // 如果发送成功,返回成功发送的字节数,否则返回SOCKET_ERROR (4) Recv int recv( SOCKET s, char* buf, //存放接受数据的缓冲区 int len, //接受缓冲区的大小 int flags //接受时使用的附加标志 ) (5) Closesocket // 用于关闭不在需要的Socket int closesocket( SOCKET s //Socket套接字 ) // 成功返回0,否则返回SOCKET_ERROR (6) Listen //Listen用于在某个Socket上建立监听,函数原型为: int listen( SOCKET s, int backlog //缓存队列的长度 ) // 成功返回0,否则返回SOCKET_ERROR // 关于backlog参数,一般理解为协议层已经开始或已经完成连接的建立,但是应用程序还没有开始接受数据时,连接的数量未知。 // 以前通常都是设为5,因为BSD4.2最大支持5,但是现在都是取SOMAXCONN,表示最大 (7) Accept // 用于接受一条新的连接,注意:是接受连接而不是接受数据,原型为 SOCKET accept( SOCKET s, //监听中的Socket struct sockaddr* addr, //表示地址结构体的指针 int* addrlen //地址结构体的长度 ) // 成功返回一个新的Socket套接字,否则返回SOCKET_ERROR (8) Bind //用于给一个Socket套接字分配一个本地协议地址,原型为 int bind( SOCKET s, const struct sockaddr* name, //表示地址结构体的指针 in namelen //地址结构体的程度 ) // 成功返回0,否则返回SOCKET_ERROR (9) Select // 用于检测Socket状态,主要用于高级的网络通信模型: int select( int nfds, //Winsock中此函数无意义 fd_set* readfds, //进行可读检测的Socket fd_set* writefds,//进行可写检测的Socket fd_set* exceptfds, //进行异常检测的Socket const struct timeval* timeout // 非阻塞模式中设置最大等待时间 ) // 成功返回0,否则返回SOCKET_ERROR
IP地址转换
主要有三种IP地址
(1) 无符号整数地址:127.0.0.1
(2) ASCII地址:“127.0.0.1”
(3) 域名地址:“localhost”转换方法:
(1) 整数地址到ASCII地址的转换 #include <arpa/inet.h> // ASCII => INT int inet_aton(const char *straddr, struct in_addr* adrp); // 0表示失败,1表示成功 // INT => ASCII char *inet_ntoa( struct in_addr inaddr); //NULL表示失败,其他任何值表示成功 (2) 域名地址与整数地址的相互转换 # include <netdb.h> // DOMAIN => INT struct hostent *gethostbyname( const char * name); // INT => DOMAIN struct hostent *gethostbyaddr( const char *addr, int len, int family);
字节转换
由于大端对齐和小端对齐的原因,不同的计算机系统在进行网络通信时需要一个统一的格式,也就是Big Endian格式,称之为“网络字节顺序”(network byte order),相对的本地计算机使用的数据格式为“本机字节顺序”(host byte order)
关于大小端对齐的问题,计算机专业的童鞋应该不会陌生,不清楚的可以看看这篇blog:
http://blog.youkuaiyun.com/yangcs2009/article/details/39698997在发送到网络之前,必须先转化为网络字节顺序,系统提供了一些相应的函数,用于字节顺序的转换,这些函数命名有一定的规律:h代表字节顺序(host或理解为本机顺序);n代表网络顺序(network)
u_long PASCAL FAR hton1 ( IN u_long hostlong); //本地字节转化为网络字节顺序(长整数) u_short PASCAL FAR htons(IN u_short hostshort); //本地字节转化为网络字节顺序(短整数) u_long PASCAL FAR ntoh1 (IN u_long netlong); //网络字节转化为本地字节顺序(长整数) u_short PASCAL FAR ntohs ( IN u_short netshort); //网络字节转化为本地字节顺序(短整数)
Q: 为什么在数据结构sockaddr_in中的sin_addr和sin_port要转换成网络字节顺序,而sin_family不需要呢?
A: 答案是:sin_addr和sin_port分别封装在包的IP 和 UDP层,但是sin_family只是被本机使用来决定数据结构中包含什么类型的地址,没有被发送到网络上基本Socket通信
服务器端基本流程:[初始化监听Socket]->[接受新的客户端连接]->[收发数据]->[关闭连接]
//!!注意:在Windows下必须包含头文件ws2tcpip.h ,否则会报错:socklen_t was not declared in this scope //!!注意:在Windows下执行socket相关操作前,必须初始化WSADATA!!!! WSADATA wsaData; int nRet; if((nRet = WSAStartup(MAKEWORD(2,2),&wsaData)) != 0){ exit(0); }
(一) 初始化监听Socket
// (1) 初始化Socket SOCKET s; s = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP) // 使用流Socket,基于TCP协议 // (2)绑定Socket SOCKADDR_IN ServerAddr; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(9000); ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); bind(s, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // (3)开始监听 listen(s, SOMAXCONN);
(二) 建立连接
// (1) 检测Socket状态:函数select可以检测相应的Socket状态,从而决定是否需要建立新的连接 fd_set readset; timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; FD_ZERO(&readset); FD_SET(s, &readset); int ret = select(FD_SETSIZE, &readset, NULL, NULL, &timeout); if(ret>0 && FD_ISSET(s, &readset)) { //新连接 } // (2) 建立新的连接:如果客户端有新的连接请求介入,则建立一个新的连接 // 注意:使用accept建立新的连接时,需要使用新的socket进行通信(temp) SOCKADDR_IN ServerAddr; int len = sizeof(ServerAddr); SOCKET temp; temp = accept(s, (SOCKADDR*) &ServerAddr, (socklen_t*)&len); if(temp == INVALID_SOCKET){ //链接失败 }
(三) 收发数据
// (1) 检测读入数据 fd_set readset; timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; FD_ZERO(&readset); FD_SET(s, &readset); int ret = select(FD_SETSIZE, &readset, NULL, NULL, &timeout); if(ret>0 && FD_ISSET(s, &readset)) { //有新数据 } // (2) 接收数据 char buf[1024]; in ret; ret = recv(s, buf, 1024, 0); // (3) 检测发送数据 fd_set sendset; timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; FD_ZERO(&sendset); FD_SET(s, &sendset); int ret = select(FD_SETSIZE, &sendset, NULL, NULL, &timeout); if(ret>0 && FD_ISSET(s, &sendset)) { //可以发送新数据 } // (4) 发送数据 char buf[1024]; int ret; ret = send(s, buf, 1024, 0);
(四) 关闭连接
closesocket(s)
客户端基本流程:[初始化监听Socket]->[建立连接]->[收发数据]->[关闭连接]
(一) 初始化监听Socket
// (1) 初始化Socket SOCKET s; s = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP) // 使用流Socket,基于TCP协议 // (2)绑定端口 SOCKADDR_IN ServerAddr; ServerAddr.sin_family = AF_INET; ServerAddr.sin_addr_s_addr = htonl(INADDR_ANY); addr.sin_port = htons(0); bind(s, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 客户端的IP地址和端口号都不需要固定,可以由系统分配,因此可以不适用bind函数绑定端口和协议 // (3) 建立连接 SOCKADDR_IN ServerAddr; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(9000); ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); connect(s, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));