TCP/IP网络编程 第一章:理解网络编程和套接字

当我们学习到OSI网络层次模型或者TCP/IP层次模型时,我们学到的是一个应用层协议的内容经过运输层.网络层和链路层层层包裹发送到另一方,然后又通过路由器和对方计算器的反复的拆解最终到达对方电脑.那么这个过程究竟是从哪里开始的呢?

套接字的定义

套接字(Socket)是计算机网络中用于实现网络通信的编程接口,也可以被视为通信端点。它提供了一种机制,使得应用程序能够通过网络进行数据的发送和接收。

套接字通常由以下信息组成:

  1. IP地址:标识一个网络上的主机或网络接口。
  2. 端口号:用于标识一个应用程序或进程。
  3. 传输协议:如TCP、UDP等。

套接字可以分为两种类型:

  1. 流套接字(Stream Socket):基于传输控制协议(TCP),提供可靠的、面向连接的通信。流套接字提供有序、无差错的数据传输,确保数据按顺序到达目的地。
  2. 数据报套接字(Datagram Socket):基于用户数据报协议(UDP),提供不可靠的、无连接的通信。数据报套接字以数据报(数据包)的形式发送和接收数据,不对数据传输进行可靠性和顺序的保证。

套接字编程接口允许开发者使用各种编程语言(如C、Java、Python等)创建、连接、发送和接收数据等操作,以实现网络通信和应用程序间的交互。通过套接字,应用程序能够在不同主机之间进行通信,实现客户端与服务器之间的数据交互。

当初学者看到上方庞大的套接字定义,说不定开始抱怨这么大一坨又长又臭的定义谁记得住啊!如果学过一点操作系统,特别是Linux的同学可能好理解一点。套接字说白了,就是一个文件,但它不是在计算机本地的,通过磁盘转两下就可以得到的。它是储存在两台甚至多台计算机上的文件。举个例子吧,一个客户-服务器端,客户将它要加工的内容放入一个文件夹中,服务器将其取得并且加工后再放入一个文件夹,最终客户从那个文件夹中取得它想要的内容。这就是套接字!

创建一个套接字

看过了套接字的定义后,我们可以学着如何创建一个套接字。

还是客户-服务器端举例,这里要注意一点,客户端创建套接字和服务器创建套接字两个过程是有些许不同的。

扫描二维码关注公众号,回复: 15785418 查看本文章

下面是服务器端创建的套接字:

int socket(int domain,int type,int protocol);//成功返回文件描述符,失败返回-1

int bind(int sockfd,struct sockaddr* myaddr,socklen_t addrlen);//成功返回0,失败返回-1

int listen(int sockfd,int backlog);//成功时返回0,失败时返回-1

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);//成功时返回文件描述符,失败时返回-1

网络连接中服务器端的套接字端创建套接字过程可概括如下:

第一步:调用socket函数创建套接字

第二步:调用bind函数分配IP地址和端口号

第三步:调用listen函数准备监听套接字

第四步:调用accept函数受理连接请求

记住上面的四步很轻松,由于我们需要和另一台计算机进行网络通信(本质上是数据交换),所以我们一定要创建一个文件用来存放交换的数据,进一步的由于一台计算机不可能只有一共网络的连接,所有我们需要唯一的标识出这个文件所属的IP地址(可以转发)和端口号(理解为所属的软件的标识),接下来,我们作为服务器端需要时时刻刻的等待(监听)这个套接字是否有连接请求,如果一旦有,我们需要创建一个全新的套接字于客户端进行连接。当新的套接字创建成功后,可以正式的开始数据交换。

下方有一个服务器套接字实践:

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

int main(int argc,char *argv[]){
    int serv_sock;
    int clnt_sock;
    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;
    char message[]="Hello World";
    
    if(argc!=2){
    printf("Usage : %s <port>\n",argv[0]);
    exit(1);
    }

    serv_sock=socker(PF_INET,SOCK_STREAM,0);
    if(serv_sock==-1){
    error_handling("socker() error");
    
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
    error_handling("bind() error");

    if(listen(serv_sock,5)==-1)
    error_handling("listen() error");

    clnt_addr_size=sizeof(clnt_addr);
    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_size);
    if(clnt_sock==-1)error_handling("accept() error");

    write(clnt_sock,message,sizeof(message));
    close(clnt_sock);
    close(serv_sock);
    return 0;
}
    void error_handling(char*message){
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}

上述就是简单的服务器端套接字模拟。

下面再来讲解以下客户端方的套接字:

int sock(int domain,int type,int protocol);//成功时返回0,失败时返回-1

int connect(int sockfd,struct sockaddr *serv_addr,socklen_t addrlen);//成功时返回0,失败时返回-1

客户端程序只有"调用socket函数创建套接字"和"调用connet函数向服务器端发送连接请求"这两个步骤,因此比较简单。

#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);
    if(sock==-1)error_handling("socker() 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;
}
voie error_handling(char*message){
    fputs(message,stderr);
    fputs('\n',stderr);
    exit(1);
}

有些细节看不懂没关系,只要大致上可以看懂即可。

基于Windows平台的实现

由于大多数项目都在Linux系列的操作系统上开发服务器端,而大多数客户端是在Windows平台下开发的。不仅如此,有时应用程序还需要再两个平台之间相互切换。因此,学习套接字编程的过程中,有必要兼顾Windows和Linux两大平台。

Windos平台和Linux平台的不同

进行Winsock编程时,首先必须调用WSAStartup函数,设置程序中用到的Winsock版本,并初始化相应版本的库。

#include <winsock2.h>
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);//成功时返回0,失败时返回非零的错误代码值。

 wVersionRequested   程序员要用的Winsock版本信息。
 lpWSAData           WSADATA结构体变量的地址值

有必要给出上述两个参数的详细说明。先说第一个,Winsock中存在多个版本,应准备WORD类型的(WORD是通过typedef声明定义的unsigned short类型)套接字版本信息,并传递给该函数的第一个参数wVersionRequested。若版本为1.2,则其中1是主版本号,2是副版本号,应传0x0201。
如前所述,高8位为副版本号,低8位为主版本号,以此进行传递。

不过,以字节为单位手动构造版本信息有些麻烦,借助MAKEWORD宏函数则能轻松构建WORD型版本信息进行构建
□ MAKEWORD(1,2);    //主版本为1,副版本为2,返回0x0201。
□ MAKEWORD(2,2); //主版本为2,副版本为2,返回0x0202。

接下来讲解第二个参数lpWSADATA,此参数中需传入WSADATA型结构体变量地址(LPWSADATA是WSADATA的指针类型)。调用完函数后,相应参数中将填充已初始化的库信息。虽无特殊含义,但为了调用函数,必须传递WSADATA结构体变量地址。下面给出WSAStartup函数调用过程,这段代码几乎已成为Winsock编程的公式

int main(int argc, char* argv[]){
    WSADATA wsaData;
    ........
    if(WSAStartup(MAKEWORD(2,2), &wsaData)
    ErrorHandling("WSAStartup() error!");
    ........
    return 0;
}

前面已经介绍了Winsock相关库的初始化方法,接下来讲解如何注销该库的函数。

#include <winsock2.h>
int WSACleanup(void);//成功时返回0,失败时返回SOCKET_ERROR。

调用该函数时,Winsock相关库将归还Windows操作系统,无法再调用Winsock相关函数。从原则上讲,无需再使用Winsock函数时才调用该函数,但通常都在程序结束之前调用。

基于Windows的套接字相关函数

#include<winsock2.h>
SOCKET socket(int af,int type,int protocol);//成功时返回套接字句柄,失败时返回INTVALID_SOCKET

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

int connnect(SOCKET s,const struct sockaddr * name,int namelen);//成功时返回0,失败时返回SOCKET_ERROR

int closesocket(SOCKET s);//专门用来关闭套接字的函数

参考书籍《TCP/IP网络编程》

猜你喜欢

转载自blog.csdn.net/Reol99999/article/details/131605309