网络编程之套接字

看完了《TCP/IP网络编程》这本书,现在分批做点总结:

什么是套接字:

百度百科的解释是这样的:
套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
非常非常简单的举例说明下:Socket=Ip address+ TCP/UDP + port
在我看来,套接字就是网络编程的接口,是连接网络的一种工具。用IP地址区分主机、用套接字区分主机里面的程序、用端口号区分套接字
可分配的端口号是0-65535,但0-1023是知名端口号,不能使用;TCP套接字和UDP套接字可以共用同一个端口号

Linux套接字创建过程

网络编程中接受连接请求的套接字创建过程可整理如下:
首先介绍一下

Linux相关知识:

1、底层文件访问(Low-Level File Access)和文件描述符

  • 底层;与标准无关的操作系统独立提供的
  • 文件描述符(对应于window的句柄):系统分配给文件或套接字的整数
  • 文件和套接字一般经过创建过程才会被分配文件描述符,输入输出对象即使未经过特殊的创建过程,程序开始运行后也会被自动分配文件描述符。

2、打开文件的函数:

    int open(const char *path, int flag); 
  • 成功时返回文件描述符,失败时返回-1.
  • path为文件名的字符串地址(路径信息),flag文件打开模式信息

3、关闭文件的函数

ssize_t write(int fd, const void * buf, size_t nbytes); 
  • 成功时返回写入的字节数,失败时返回-1
  • fd,显示数据传输对象的文件描述符
  • buf,保存要传输数据的缓冲地址值
  • nbytes,要传输数据的字节数
  • size_t是通过typedef声明的unsigned int类型,ssize_t代表signed int,
  • s代表signed,ssize_t、size_t都是元数据类型,操作体统定义的数据类型会添加后缀_t

4、创建文件的函数

fd=open("data.txt",O_CREAT|O_WRONLY|O_TRUNC);

创建空文件,只写模式打开,若存在同名文件,则清空文件全部数据

5、读取文件的函数

  ssize_t read(int fd, void * buf, size_t nbytes); 
  • 成功时返回接收的字节数(文件结尾返回0),失败时返回-1
  • 文件描述符从3开始以由小到大的顺序编号,0,1,2是分配给标准I/O的描述符

服务器端:
1、socket函数创建套接字

  #include <sys/socket.h> 
  int socket(int domain, int type, int protocol); 
  • 成功时返回文件描述符,失败时返回-1.
  • domain,套接字中使用的协议族信息;
  • type,套接字数据传输类型信息;
  • protocol,计算机间通信中使用的协议信息
  • 协议族(Protocol Family): PF_INET,IPV4互联网协议族 ;PF_INET6,IPV6互联网协议族
  • PF_LOCAL,本地通信的Unix协议族 PF_PACKET,底层套接字的协议族 PF_IPX,IPX Novell协议族

套接字实际采用的最终协议信息是通过socket函数的第三个函数传递, 在指定协议族范围内通过第一个参数决定第三个参数

套接字类型(Type)
即套接字的数据传输方式,socket第二个参数传递,协议族中可能存在多种数据传输方式

1、面向连接的套接字(基于字节)
向socket第二个参数传递SOCK_STREAM,创建面向连接的套接字
特点:

  1. 传输过程中数据不会丢失、按序传输数据、传输的数据不存在数据边界,
  2. 套接字连接必须一一对应
  3. 收发数据的套接字内部有缓冲(buffer),即字节数组 面向连接的套接字会根据接收端的状态传输数据
    如:
  int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

2、面向消息的套接字
SOCK_DGRAM

  1. 不可靠、不按顺序、无连接、有数据边界、以高速为目的
  2. 限制每次传输的数据大小
    int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
    协议的最终选择
    socket函数的第三个参数决定最终采用的协议,第三个参数大多取0,除非同一协议族中存在多个数据传输方式相同的协议

2、bind函数分配地址信息(IP地址和端口号)

  #include <sys/socket.h>  
  int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);  
  • 成功返回0,失败返回-1。
  • sockfd 要分配地址信息(IP地址和端口号)的套接字文件描述符
  • myaddr 存有地址信息的结构体变量地址值
  • adddrien 第二个结构体变量的长度
  • 地址信息的表示
  • AF_INET,IPV4网络协议中使用的地址族
  • AF_INET6,IPV6网络协议中使用的地址族

相关结构体

struct sockaddr_in 
{ 
  sa_family_t    sin_family;//地址族 
  uint16_t       sin_port;//16位tcp/udp端口号,以网络字节序保存 
  struct in_addr sin_addr;//32位IP地址,以网络字节序保存 
  char           sin_zero[8];//不使用,必需为0, 
                                   为使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员 
};
struct in_addr  
{ 
 in_addr_t  s_addr;//32位IPV4地址 
};

POSIX是UNIX系列操作系统的可移植操作系统接口,定义了一些其他数据类型
- sa_family_t 地址族
- socklen_t 长度
- in_addr_t IP地址,声明为uint32_t
- in_port_t 端口号,声明为uint16_t

网络字节序与地址变换
不同CPU中,4字节整数型值1在内存空间的保存方式是不同的
字节序和网络字节序:
大端序:高位字节存放到低位地址 【高位字节值存放到低位地址,先存高位值】
小端序:高位字节存放到高位地址
IntelCPU系列用小端
网络字节序统一为大端序

字节序转换:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
h代表主机,n代表网络,s指short,l指long,to就是to
除了向sockaddr_in结构体变量填充数据外,其他情况无需考虑字节序问题

网络地址的初始化和分配
ockaddr_in中保存的地址信息的成员为32位整数型

#include <arpa/inet.h> 
in_addr_t inet_addr(const char * string); 

成功返回 32位大端序整数型值,失败时返回INADDR_NONE,还可以检测无效IP

#include <arpa/inet.h> 
int inet_aton(const char * string, struct in_addr * addr); 
  • 成功返回1,失败返回0
  • string 含有需转换的IP地址信息的字符串地址值
  • addr 将保存转换结果的in_addr结构体变量的地址值

aton和addr函数功能完全相同,利用了in_addr结构体,使用频率更高,但Windows没有此函数

网络地址初始化【主要针对服务器】

struct sockaddr_in addr; 
char * serv_ip = "211.217.168.13"; //声明IP地址字符串
char * serv_port = "9190";  //声明端口号字符串
memset(&addr, 0, sizeof(addr)); //结构体变量addr的所有成员置零
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化 
addr.sin_port = htons(atoi(serv_port)); //基于字符串的端口号初始化

利用字符串格式的IP地址和端口号初始化了sockaddr_in结构体变量

客户端地址信息初始化
声明sockaddr_in结构体,初始化为要与之连接的服务器端套接字的IP和端口号,
再调用connect

INADDR_ANY 
addr.sin_addr.s_addr = htonl(INADDR_ANY); 

自动获取运行服务器端的计算机IP地址
同一计算机可以分配多个IP地址,个数与NIC相等
3、listen函数设置监听【将套接字转化为可接收请求状态】
#include

  #include <sys/socket.h> 
  int accept(int sockfd, struct sockaddr *addr,  
  socklen_t *addrlen); 
  • 成功时返回文件描述符,失败时返回-1.
  • sock 服务器套接字的文件描述符
  • addr 保存发起连接请求的客户端地址信息的变量地址值
  • 调用函数后向传递来的地址变量参数填充客户端地址信息
  • addrlen 第二个参数addr结构体的长度,但是存有长度的变量地址。
  • 函数调用完成后,该变量即被填入客户端地址长度

函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符,套接字自动创建并自动与发起连接的客户端建立连接

客户端:

#include <sys/socket.h> 
          int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen); 
  • 成功返回0,失败返回-1。
  • sock 客户端套接字的文件描述符
  • servaddr 保存目标服务器端地址信息的变量地址值
  • addrlen 以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度

    客户端调用connect函数后,服务器端接收连接请求或中断连接请求才会返回
    客户端的IP地址和端口在调用connect函数时自动分配
    以下是一份简单的代码:

#include <stdio.h>  
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h>  
#include <arpa/inet.h> 
#include <sys/socket.h> 
void error_handling(char *message);  


int main(int argc, char* argv[]) 
{  
  int sock; 
  struct sockaddr_in serv_addr; 
  char message[30]; 
  int str_len; 

  if(argc!=3) 
  { 
    printf("Usage : %s <IP> <port>\n", argv[0]); 
    exit(1); 
  } 

  sock=socket(PF_INET, SOCK_STREAM, 0); //建立TCP套接字
  if(sock == -1) 
     error_handling("socket() error"); 

  memset(&serv_addr, 0, sizeof(serv_addr)); //地址信息初始化
  serv_addr.sin_family=AF_INET; 
  serv_addr.sin_addr.s_addr=inet_addr(argv[1]); 
  serv_addr.sin_port=htons(atoi(argv[2])); 

  if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)//请求连接 
     error_handling("connect() error!");  

  str_len=read(sock, message, sizeof(message)-1); //读取数据
  if(str_len==-1) 
     error_handling("read() error!"); 

  printf("Message from server : %s \n", message); 
  close(sock); //关闭套接字
  return 0; 
}  

void error_handling(char *message) 
{ 
  fputs(message, stderr); 
  fputc('\n', stderr); 
  exit(1);
}

Windows套接字创建过程:

1、winsock编程时,首选必须调用WSAStartup函数

#include <winsock2.h> 
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); 

成功时返回0,失败时返回非零的错误代码值
wVersionRequested,程序员要用的Winsock版本信息
lpWSAData,WSADATA结构体变量的地址值
套接字版本信息应该准备WORD(unsigned short)类型的,传递给该函数第一个参数wVersionRequested,版本号为1.2,则传递0x0201
可用MAKEWORD宏函数构建WORD型版本信息
MAKEWORD(1, 2);版本为1.2;

2、注销库

#include <winsock2.h> 
int WSACleanup(void); 

成功时返回0,失败时返回SOCKET_ERROR
调用该函数,winsock相关库将归还给Windows操作系统,多在程序结束之前调用

3、相关函数
创建套接字

#include <winsock2.h> 
SOCKET socket(int af, int type, int protocol); 

成功时返回套接字句柄,失败时返回INVALID_SOCKET


分配IP和端口

int bind(SOCKET s, const struct sockaddr * name, int namelen); 

成功时返回0,失败返回SOCKET_ERROR


激活套接字,使可接收客户端连接

int listen(SOCKET s,int backlog); 

成功时返回0,失败返回SOCKET_ERROR


调用受理客户端连接请求

SOCKET accept(SOCKET s,struct sockaddr * addr, int * addrlen); 

成功时返回套接字句柄,失败时返回INVALID_SOCKET


客户端发送连接请求

connect(SOCKET s, const struct sockaddr * name,int namelen); 

关闭套接字

int closesocket(SOCKET s); 

成功返回0,失败返回SOCKET_ERROR


不同于Linux,Windows的文件句柄相关函数和套接字句柄相关函数是有区别的

4、基于Windows的I/O函数
Windows严格区分套接字I/O函数和文件I/O函数
Windows数据传输函数

int send(SOCKET s, const char * buf, int len, int flags); 

成功返回传输字节数,失败返回SOCKET_ERROR
s,数据传输对象连接的套接字句柄值
buf,保存待传输数据的缓冲地址值
len,要传输的字节数
flag,传输数据时用到的多种选项信息


int recv(SOCKET s,const char * buf,int len,int flags); 

成功返回接收的字节数(收到EOF返回0),失败返回SOCKET_ERROR


以下是Windows的代码:

#include <stdio.h>  
#include <stdlib.h>  
#include <WinSock2.h>  
void ErrorHandling(char* message);  

int main(int argc, char *argv[]) {  
    WSADATA wsaData;  
    SOCKET hSocket;  
    SOCKADDR_IN servAddr;  

    char message[30];  
    int strlen;  
    if (argc != 3) {  
        printf("Usage: %s <IP> <port>\n", argv[0]);  
        exit(1);  
    }  

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  
        ErrorHandling("WSAStartup() error!");  

    hSocket = socket(PF_INET, SOCK_STREAM, 0);  
    if (hSocket == INVALID_SOCKET)  
        ErrorHandling("socket() error!");  

    memset(&servAddr, 0, sizeof(servAddr));  
    servAddr.sin_family = AF_INET;  
    servAddr.sin_addr.s_addr = inet_addr(argv[1]);  
    servAddr.sin_port = htons(atoi(argv[2]));  

    if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)  
        ErrorHandling("connect() error!");  

    strlen = recv(hSocket, message, sizeof(message) - 1, 0);  
    if (strlen == -1)  
        ErrorHandling("read() error!");  

    printf("Message from server:%s\n", message);  
    closesocket(hSocket);  
    WSACleanup();  
    return 0;  
}  

void ErrorHandling(char * message) {  
    fputs(message, stderr);  
    fputc('\n', stderr);  
    exit(1);  
}  

猜你喜欢

转载自blog.csdn.net/qq_40194498/article/details/80242662