socket编程-基础API

Linux下使用C++进行socket编程。

创建socket

socket

在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件,UNIX/Linux 中的一切都是文件。为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个文件描述符,而网络连接也是一个文件,它也有文件描述符。而Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。通过socket()函数可以创建一个网络连接,得到的返回值就是对应的文件描述符

Internet套接字

通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。最常用的是流格式套接字数据报格式套接字

流格式套接字SOCK_STREAM

流格式套接字是“面向连接的套接字”,对应TCP,SOCK_STREAM是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

SOCK_STREAM有以下特点:

  • 数据按序到达
  • 发送数据没有数据边界
  • 数据传输过程中不会丢失

SOCK_STREAM类型的套接口为全双向的字节流,在接收或发送数据前必需处于已连接状态。用connect()调用建立与另一套接口的连接,连接成功后,即可用send()和recv()传送数据。当会话结束后,调用closesocket()。带外数据根据规定用send()和recv()来接收。

数据报格式套接字SOCK_DGRAM

数据报格式套接字是“无连接的套接字”,对应UDP,SOCK_DGRAM,只管传输数据,不作数据校验,若数据损坏不会重传。

SOCK_DGRAM有以下特点:

  • 数据快速传输,可能乱序
  • 每次传输数据比较少
  • 发送数据有数据边界

SOCK_DGRAM类型套接口允许使用sendto()和recvfrom()从任意端口发送或接收数据报。如果这样一个套接口用connect()与一个指定端口连接,则可用send()和recv()与该端口进行数据报的发送与接收。

创建socket

int socket(int domain,int type,int protocol)
  • domain: 指明系统要使用的底层协议族,对于TCP/IP协议族而言设置为PF_INET,对于UNIXben地域协议族而言设置为PF_UNIX

  • type:socket的类型,如SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)

  • protocol:协议号,默认为0,常用协议为IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。一般情况下有了 domain 和 type 两个参数就可以创建socket了,系统会自动推演出协议类型。

若无错误发生,socket()返回引用新套接口的描述字。否则的话,返回INVALID_SOCKET错误,应用程序可通过WSAGetLastError()获取相应错误代码。

示例代码:

int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议
//上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议
int tcp_socket2 = socket(PF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket2 = socket(PF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

加载socket库

在Windows条件下需要先加载套接字库
WSAStartup,即WSA(Windows Sockets Asynchronous,Windows异步套接字)的启动命令,WSAStartup必须是应用程序或DLL调用的第一个Windows Sockets函数,允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节。

#include "winsock.h" 
#include "windows.h" 
#pragma comment(lib,"ws2_32.lib")   //静态加入一个lib文件
    
    WORD sockVersion = MAKEWORD(2, 2);
    /*
    WORD是微软SDK中的无符号16位整形数,MAKEWORD(a,b)是一个宏,这里用来指定使用的Winsock版本	,MAKEWORD(2,2)即版本2.2
    */
	WSADATA wsaData;//WSADATA 结构被用来保存函数 WSAStartup 返回的 Windows Sockets初始化信息
	if (WSAStartup(sockVersion, &wsaData) != 0) {
		/*
		使用 Socket 的程序在使用 Socket 之前必须调用 WSAStartup 函数, 当一个应用程序调用 WSAStartup 函数时,
		操作系统根据请求的 Socket 版本来搜索相应的 Socket 库,
		然后绑定找到的 Socket 库到该应用程序中。以后应用程序就可以调用所请求的 Socket 库中的其它 Socket 函数了。
		 */
		printf_s("WSAStartup failed.\n"); // 初始化失败 
		exit(1);
	}
    /*socket相关操作
    ....
    */
        
	WSACleanup();//使用完socket后使用WSACleanup()函数来主动释放资源

而在Linux条件下的头文件为

#include<sys/types.h>
#include<sys/socket.h>

命名socket

bind(int sockfd,const struct sockaddr* addr,int addrlen);
  • sockfd :socket 文件描述符
  • addr: sockaddr 结构体变量的指针
  • addrlen: addr 变量的大小,可由 sizeof() 计算得出。

bind()函数通过给一个未命名socket分配一个本地名字(sockaddr)来为套接口建立本地绑定。bind失败时返回-1并设置errno,成功时返回0。
地址相关的操作见socket地址API

  • errno == EACCES:被绑定的地址为受保护的地址,只有root用户能够绑定,例如绑定到了知名端口上
  • errno == EADDRINUSE:被绑定的地址正在使用
/*创建一个地址*/
struct sockaddr_in address;
bzero(&address, sizeof( address ));
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
/*创建socket*/
int sock = socket( PF_INET , SOCK_STREAM , 0);
/*绑定socket和address*/
int ret = bind( sock , (struct sockaddr * )&address , sizeof( address));

监听socket

int listen( int sockfd ,int backlog);
  • sockfd:被监听的socket文件描述符
  • backlog:内核监听队伍的长度

对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数为socket创建一个监听队伍来存放要处理的客户连接,再调用 accept() 函数,就可以随时响应客户端的请求了。监听队伍的长度由backlog决定,当监听队伍满时无法再处理新的客户连接。

接受连接

int accept( int sockfd , struct sockaddr* addr , socklen_t * addrlen);
  • sockfd:执行accpet调用的监听socket
  • addr:用于保存获取的连接的远端socket地址
  • addrlen:socket地址的长度

处于监听状态(listen)的socket通过调用accept()可以从监听队伍中取出一条连接,对应的返回值就是用于与客户端通信的socket,下面的读写操作均通过accept返回的socket进行通信。

struct sockaddr_in client; // 用于接收连接的sockaddr
socklen_t client_addrlength = sizeof( client ); // sockaddr的长度
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
	printf( "errno is: %d\n", errno );
}
else
{
	char remote[INET_ADDRSTRLEN ];
	printf( "connected with ip: %s and port: %d\n", 
	inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ),
	//inet_ntop将网络序存储的IP(sin_addr)转为char*并存储在remote中,然后返回
	ntohs( client.sin_port ) );
	
	/* ... */
}

accept操作只会从监听队伍中取出连接,而不关心连接处于什么状态(即使客户端已经断开了连接)

发起连接

int connect( int sockfd , const struct sockaddr * serv_addr , socklen_t addrlen );
  • sockfd:客户端用于连接服务器的socket
  • serv_addr:服务器监听的socket地址,也就是客户端要连接的地址
  • addrlen:socket地址的长度

客户端通过connect()函数来主动与服务器建立连接,connect()成功时返回0,此后通过sockfd来和服务器进行通信,失败时返回-1,同时设置errno:

  • errno == ECONNREFUSED:目标端口不存在,连接被拒接
  • errno == ETIMEOUT:连接超时

关闭连接

int close( int fd );
int shutdown( int sockfd , int howto );

close()系统调用并不是立即关闭一个连接,而是将fd的引用计数减1,当fd的引用计数为0时才真正关闭连接。多进程时fork()会导致fd的引用计数加1,因此必须在父进程和子进程中都调用close()才能关闭连接。

为此引入了shutdown()系统调用,会强制关闭socket,howto参数决定了shutdown()的行为。

数据读写

TCP读写 - send()/recv()

ssize_t recv( int sockfd , void *buf, size_t len, int flags );

ssize_t send( int sockfd , const void *buf, size_t len, int flags );
  • sockfd:通信用的socket
  • buf:发送/接收缓冲区
  • len:缓冲区的大小
  • flags:为数据收发提供了额外的控制,一般设置为0/NULL

recv()读取sockfd上的数据,成功时返回读取到的数据的长度,可能需要调用多次recv()才能读到完整的数据;recv()返回0时代表对方已经关闭连接;recv()返回-1说明出错并会设置errno。默认情况下recv()为阻塞I/O,当没有数据时会阻塞等待。

send()向sockfd上写数据,返回值和recv()定义的一致。send()默认也是阻塞I/O,当内核写缓冲满时会阻塞等待。

UDP读写 - sendto()/recvfrom()

ssize_t recvfrom( int sockfd , void *buf, size_t len, int flags ,
struct sockaddr *src_addr , socklen_t addrlen );

ssize_t sendto( int sockfd , const void *buf, size_t len, int flags ,
const struct sockaddr *dest_addr , socklen_t addrlen );

由于UDP中没有建立连接,所以每次读写数据都要获取发送方/接收方的socket地址。

recvfrom()用于读取sockfd上的数据,用法与recv()一样,额外的参数 src_addr 和 addrlen 用于指明发送方的socket地址。

sendto()用于通过sockfd发送数据,用法和send()一样,额外的参数 dest_addr 和 addrlen 用于指明接收方的socket地址。

socket编程实例

socket编程实例

posted @ 2020-07-07 14:52  海物chinono  阅读(271)  评论(0)    收藏  举报