赞
踩
本文将在上一篇的基础上介绍Socket套接字的应用。
下面逐一介绍一下这些API函数;
应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段。
int socket(int domain, int type, int protocol);
参数:
domain:即协议域,又称为协议族(family)。
常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:创建的套接字的类型,常用SOCK_STREAM(流式套接字),SOCK_DGRAM(数据报套接字)和SOCK_RAW(原始套接字)。
(1)TCP流式套接字(SOCK_STREAM)提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。
(2)数据报式套接字(SOCK_DGRAM)提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
(3)原始式套接字(SOCK_RAW)该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
protocol: 说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。
返回值: 成功:返回指向新创建的socket的文件描述符,是socket的ID值,标识socket的唯一性,是一个整形数;失败:返回-1。
将套接字和ip地址和端口绑定,当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:存入网络类型,网络地址和端口号的结构体。一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
- struct sockaddr_in
- {
- sa_family_t sin_family; //地址族(Address Family),也就是地址类型
- uint16_t sin_port; //16位的端口号
- struct in_addr sin_addr; //32位IP地址
- char sin_zero[8]; //不使用,一般用0填充
- };
-
- struct in_addr
- {
- in_addr_t s_addr; //32位的IP地址
- };
-
- struct sockaddr
- {
- sa_family_t sin_family; //地址族(Address Family),也就是地址类型
- char sa_data[14]; //IP地址和端口号
- };
结构体sockaddr和sockaddr_in等价,sockaddr_in包含详细的ip地址和端口号,所以sockaddr_in更加利于阅读和使用,编程的时候多使用它。详见:sockaddr和sockaddr_in详解_爱橙子的OK绷的博客-CSDN博客
addrlen:addr结构体的长度。
返回值:如果没有错误发生,bind()返回0。否则返回SOCKET_ERROR。
在使用bind时常用的两个函数:htons和htonl,在将一个地址绑定到socket的时候,先将主机字节序转换成为网络字节序。
- erv_addr.sin_port = htons(LISTEN_PORT);
- serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
IP地址“127.0.0.1”这是点分十进制形式的字符串形式,而在结构体struct sockaddr_in 中IP地址是以32位(即4字节整形类型)数据保存的,这时我们可以调用 inet_aton() 函数将点分十进制字符串转换成 32位整形类型。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
int listen(int sockfd,int backlog);
参数:
sockfd:一个正在用于监听功能下的套接字的文件描述符。
backlog:backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。
返回值:listen()返回0。否则它返回SOCKET_ERROR。
调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。accept()用于使服务器等待来自某客户进程的实际连接。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
各参数和bind函数一样,区别在于,connect函数是client端用于和server端建立连接。
sockfd: 客户端的socket()创建的描述字
addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息
addrlen: socket地址的长度
返回值:如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
它的参数与 listen() 和 connect() 是相同的
参数:
sockfd:一个正在用于监听功能下的套接字的文件描述符。
addr:用于储存接受到的客户端的网络信息的结构体(参考bind下的使用)
addrlen:addr结构体长度
返回值:SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。
accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
- int send(SOCKET sock, const char *buf, int len, int flags);
-
- int recv(SOCKET sock, char *buf, int len, int flags);
参数:
sock :要发送数据的套接字
buf: 要发送的数据的缓冲区地址
len :要发送的数据的字节数
flags :发送数据时的选项,一般为0。
返回值:返回总共发送/接收的字节数。否则它返回SOCKET_ERROR。
网络I/O操作有下面几组:
- ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
- ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
- const struct sockaddr *dest_addr, socklen_t addrlen);
- ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
- struct sockaddr *src_addr, socklen_t *addrlen);
-
- ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
- ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。
recv 比read 的功能强大点,体现在recv提供的flags参数上,
recv最终的实现还是要调用read。
recv和read都可以操作阻塞或非阻塞,阻塞非阻塞与recv和read没关系,它是socket的属性,函数fcntl可以设置。
close()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
int close(int fd);
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
图示过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
- #include <stdio.h>
- #include <winsock2.h>
- #pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
- #define BUF_SIZE 100
- int main()
- {
- WSADATA wsaData;
- WSAStartup( MAKEWORD(2, 2), &wsaData);
-
- //创建套接字
- SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
-
- //绑定套接字
- sockaddr_in sockAddr;
- memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
- sockAddr.sin_family = PF_INET; //使用IPv4地址
- sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
- sockAddr.sin_port = htons(1234); //端口
- bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
-
- //进入监听状态
- listen(servSock, 20);
-
- //接收客户端请求
- SOCKADDR clntAddr;
- int nSize = sizeof(SOCKADDR);
- SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
- char buffer[BUF_SIZE]; //缓冲区
- int strLen = recv(clntSock, buffer, BUF_SIZE, 0); //接收客户端发来的数据
- send(clntSock, buffer, strLen, 0); //将数据原样返回
-
- //关闭套接字
- closesocket(clntSock);
- closesocket(servSock);
- //终止 DLL 的使用
- WSACleanup();
- return 0;
- }
- #include <stdio.h>
- #include <stdlib.h>
- #include <WinSock2.h>
- #pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
-
- #define BUF_SIZE 100
-
- int main(){
- //初始化DLL
- WSADATA wsaData;
- WSAStartup(MAKEWORD(2, 2), &wsaData);
-
- //创建套接字
- SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
-
- //向服务器发起请求
- sockaddr_in sockAddr;
- memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
- sockAddr.sin_family = PF_INET;
- sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
- sockAddr.sin_port = htons(1234);
- connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
- //获取用户输入的字符串并发送给服务器
- char bufSend[BUF_SIZE] = {0};
- printf("Input a string: ");
- scanf("%s", bufSend);
- send(sock, bufSend, strlen(bufSend), 0);
- //接收服务器传回的数据
- char bufRecv[BUF_SIZE] = {0};
- recv(sock, bufRecv, BUF_SIZE, 0);
-
- //输出接收到的数据
- printf("Message form server: %s\n", bufRecv);
-
- //关闭套接字
- closesocket(sock);
-
- //终止使用 DLL
- WSACleanup();
-
- system("pause");
- return 0;
- }
注意:在TCP连接中,client端的socket的ip和端口没有指定,在建立连接的时候,TCP协议会分配一个端口号,且运行时应当先起服务端再起客户端。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。