当前位置:   article > 正文

TCP/IP——Socket网络编程应用及实例_socket 网络程序设计

socket 网络程序设计

本文将在上一篇的基础上介绍Socket套接字的应用。

一、Socket客户端与服务端连接过程

下面逐一介绍一下这些API函数;

1.1 API介绍

1.1.1 socket()——创建套接字

        应用程序在使用套接字前,首先必须拥有一个套接字,系统调用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。

1.1.2 bind()——指定本地地址

        将套接字和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对应的是:

  1. struct sockaddr_in
  2. {
  3. sa_family_t sin_family; //地址族(Address Family),也就是地址类型
  4. uint16_t sin_port; //16位的端口号
  5. struct in_addr sin_addr; //32位IP地址
  6. char sin_zero[8]; //不使用,一般用0填充
  7. };
  8. struct in_addr
  9. {
  10. in_addr_t s_addr; //32位的IP地址
  11. };
  12. struct sockaddr
  13. {
  14. sa_family_t sin_family; //地址族(Address Family),也就是地址类型
  15. char sa_data[14]; //IP地址和端口号
  16. };

结构体sockaddr和sockaddr_in等价,sockaddr_in包含详细的ip地址和端口号,所以sockaddr_in更加利于阅读和使用,编程的时候多使用它。详见:sockaddr和sockaddr_in详解_爱橙子的OK绷的博客-CSDN博客

addrlen:addr结构体的长度。

返回值:如果没有错误发生,bind()返回0。否则返回SOCKET_ERROR。

在使用bind时常用的两个函数:htonshtonl,在将一个地址绑定到socket的时候,先将主机字节序转换成为网络字节序。

  1. erv_addr.sin_port = htons(LISTEN_PORT);
  2. serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

IP地址“127.0.0.1”这是点分十进制形式的字符串形式,而在结构体struct sockaddr_in 中IP地址是以32位(即4字节整形类型)数据保存的,这时我们可以调用 inet_aton() 函数将点分十进制字符串转换成 32位整形类型。

1.1.3 listen()——监听连接

        所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。

int listen(int sockfd,int backlog);

参数:

sockfd:一个正在用于监听功能下的套接字的文件描述符。
backlog:backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。

返回值:listen()返回0。否则它返回SOCKET_ERROR。

调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。

1.1.4 connect()与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() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

1.1.5 send()与recv()——数据传输函数

  1. int send(SOCKET sock, const char *buf, int len, int flags);
  2. int recv(SOCKET sock, char *buf, int len, int flags);

参数:

sock :要发送数据的套接字

buf: 要发送的数据的缓冲区地址

len :要发送的数据的字节数

flags :发送数据时的选项,一般为0。

返回值:返回总共发送/接收的字节数。否则它返回SOCKET_ERROR。

1.1.6 read()、write()等函数

网络I/O操作有下面几组:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()
  1. ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  2. ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  3. ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
  4. const struct sockaddr *dest_addr, socklen_t addrlen);
  5. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
  6. struct sockaddr *src_addr, socklen_t *addrlen);
  7. ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
  8. 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表示 网络连接出现了问题(对方已经关闭了连接)。

1.1.7 recv和read|send和write的区别

recv 比read 的功能强大点,体现在recv提供的flags参数上,
recv最终的实现还是要调用read。
recv和read都可以操作阻塞或非阻塞,阻塞非阻塞与recv和read没关系,它是socket的属性,函数fcntl可以设置。

1.1.8 close()——关闭套接字

        close()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

int close(int fd);

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

二、Socket三次握手与四次挥手

2.1 socket三次握手建立连接

我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

  • 客户端向服务器发送一个SYN J
  • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
  • 客户端再想服务器发一个确认ACK K+1

只有就完了三次握手,但是这个三次握手发生在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返回,至此三次握手完毕,连接建立。

2.2 socket四次挥手释放连接

图示过程如下:

  • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;

  • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;

  • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;

  • 接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

三、示例代码

3.1 服务器端代码

  1. #include <stdio.h>
  2. #include <winsock2.h>
  3. #pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
  4. #define BUF_SIZE 100
  5. int main()
  6. {
  7. WSADATA wsaData;
  8. WSAStartup( MAKEWORD(2, 2), &wsaData);
  9. //创建套接字
  10. SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
  11. //绑定套接字
  12. sockaddr_in sockAddr;
  13. memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
  14. sockAddr.sin_family = PF_INET; //使用IPv4地址
  15. sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
  16. sockAddr.sin_port = htons(1234); //端口
  17. bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
  18. //进入监听状态
  19. listen(servSock, 20);
  20. //接收客户端请求
  21. SOCKADDR clntAddr;
  22. int nSize = sizeof(SOCKADDR);
  23. SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
  24. char buffer[BUF_SIZE]; //缓冲区
  25. int strLen = recv(clntSock, buffer, BUF_SIZE, 0); //接收客户端发来的数据
  26. send(clntSock, buffer, strLen, 0); //将数据原样返回
  27. //关闭套接字
  28. closesocket(clntSock);
  29. closesocket(servSock);
  30. //终止 DLL 的使用
  31. WSACleanup();
  32. return 0;
  33. }

3.2 客户端代码

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <WinSock2.h>
  4. #pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
  5. #define BUF_SIZE 100
  6. int main(){
  7. //初始化DLL
  8. WSADATA wsaData;
  9. WSAStartup(MAKEWORD(2, 2), &wsaData);
  10. //创建套接字
  11. SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  12. //向服务器发起请求
  13. sockaddr_in sockAddr;
  14. memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
  15. sockAddr.sin_family = PF_INET;
  16. sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
  17. sockAddr.sin_port = htons(1234);
  18. connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
  19. //获取用户输入的字符串并发送给服务器
  20. char bufSend[BUF_SIZE] = {0};
  21. printf("Input a string: ");
  22. scanf("%s", bufSend);
  23. send(sock, bufSend, strlen(bufSend), 0);
  24. //接收服务器传回的数据
  25. char bufRecv[BUF_SIZE] = {0};
  26. recv(sock, bufRecv, BUF_SIZE, 0);
  27. //输出接收到的数据
  28. printf("Message form server: %s\n", bufRecv);
  29. //关闭套接字
  30. closesocket(sock);
  31. //终止使用 DLL
  32. WSACleanup();
  33. system("pause");
  34. return 0;
  35. }

注意:在TCP连接中,client端的socket的ip和端口没有指定,在建立连接的时候,TCP协议会分配一个端口号,且运行时应当先起服务端再起客户端。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/147243
推荐阅读
相关标签
  

闽ICP备14008679号