Linux C++ Socket编程入门

5 篇文章 0 订阅
1 篇文章 0 订阅

1. 前言

    在看着这篇博客之前,希望各位还是有一些计算机网络基础。还有一些C++的入门基础,不然会看的一头雾水。

2. 套接字简单介绍

    目前国际比较通用的为五层协议,即物理层,数据链路层,网络层(IP),传输层(TCP/UDP),应用层。

    下层为上层提供服务,上层的实现有需要下层为其提供服务。这里就不做深入的介绍,这要将个套接字。

    在运输层中,TCP连接的端点称为套接字或插口,端口号拼接到IP地址则形成套接字。

套接字 socket = {IP地址:端口号}

    每一条TCP连接唯一地被通信两端的两个端点(两个套接字)所确定,即:

TCP连接 ::= {socket1,socket2} = {(IP1:PORT1),(P2:PORT2)}

3. 端口号的介绍

    端口号分为两类:

    (1)服务器使用的端口号:

    包括熟知端口号或系统端口号,数值为0~1023。

常用的熟知端口号
应用程序  FTPTELNETSMTPDNSTFTPHTTPSNMPSNMP(TRAP)HTTPS
熟知端口号212325536980161162443

    以及登记端口号,数值为1024~49151。

    (2)客户端使用的端口号:数值为49152~65535


4. 创建Socket套接字

/* Create a new socket of type TYPE in domain DOMAIN, using
   protocol PROTOCOL.  If PROTOCOL is zero, one is chosen automatically.
   Returns a file descriptor for the new socket, or -1 for errors.  */
extern int socket (int __domain, int __type, int __protocol) __THROW;

这是C++Socket函数的原型,定义于<sys/socket.h>中。

参数 :

__domain为地址家族,一般填写AF_NET

__type为类型,比如TCP则为SOCKET_STREM,UDP则为SOCKET_DGRAM。(觉得这一点和python很想。虽然Java连接socket的时候是直接选择TCP类或UDP类然后给予IP地址和端口号就可以了。)

__protocol为协议编号,一般为0。(说真的,我也不知道这个是干嘛的!)

返回:

一个套接字的标识符。每个套接字都有唯一的标识符。


5. 绑定IP地址(bind)

/* Give the socket FD the local address ADDR (which is LEN bytes long).  */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
     __THROW;

像前面说的那样,要是两个地方的主机连接起来,需要有IP地址和端口号来识别。IP地址确定主机,端口号确定主机里面的进程,即需要把数据交付给哪个主机的哪个进程。bind可以将socket和IP地址绑定起来,意思差不多就是这样。

参数:

__fd为套接字的唯一标识符,就是上面的返回值。

__addr简单的理解为一个封装了IP地址和端口号的结构体。以下会继续介绍。

__len为上面__addr的长度,直接sizeof(struct sockaddr),我这里addr的类型为struct sockaddr。

返回:

这个我不清楚,以后有需要看到再说。


6. 封装IP地址和端口号的结构体

/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

先看这个原型sockaddr(位于头文件<bits/socket.h>中),这里sa_表示地址家族,就像前面提到的AF_NET。

sa_data[14]表示2位的端口号,4位的IP地址,8位的填充。

咋一看这个结构体挺好的,封装了IP地址和端口号。其实这个有一个不足,就是你要把sa_data转化为IP地址和端口的时候需要自己实现,并没有一个函数直接将其转化成IP地址和端口号。

鉴于以上的缺点,有下面更好的封装IP地址和端口号的结构体:

/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

这个sockaddr_in结构体位于<netinet/in.h>中,介绍一下成员变量:

sin_为地址家族,就像前面提到的AF_NET。

sin_port为端口号

sin_addr为IP地址,形如

sin_zero为多余的填充字段,一般是没用的

这张图可以形象生动的表示这个结构体。


下面是in_addr结构体的定义:

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

这样把连接和绑定讲清楚了。


7. 服务器监听连接(listen)

/* Prepare to accept connections on socket FD.
   N connection requests will be queued before further requests are refused.
   Returns 0 on success, -1 for errors.  */
extern int listen (int __fd, int __n) __THROW;

函数原型如上,解释一下参数:

    __fd为套接字的唯一标识符。

    __n为可以建立连接的端口数

有什么用呢?这就好比现在有一台服务器在那里,你要怎么让它和世界各地的主机相连呢?你肯定需要开放端口吧,然后监听一下有谁要来连接。这个listen的功能就是这样。不知道能不能理解?说白了,就是我告诉别人,喂喂,我是socket,你们可以来访问我,不过我只有10数量(__n = 10)的队列,先到先得,然后我登记一下。这样,服务器就有了监听别的主机访问的能力。


8. 连接绑定(connect)

/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
   For connectionless socket types, just set the default address to send to
   and the only address from which to accept transmissions.
   Return 0 on success, -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);

这是定义,解释一下参数:

    __fd为套接字的唯一标识符,不解释了。

    __addr为地址,可以是sockaddr_in或者sockaddr,其实最后都需要转化成sockaddr。

    __len为地址的sizeof,直接sizeof(sockaddr的对象)

解释一下这个有什么用,这就好比服务器开启了等待连接模式,就是需要有人来和它连接,这时候客户端就可以使用connect来建立与这个IP地址的连接,那么这个socket就变成了和服务器连接的那个套接字,和前面说的一样TCP建立连接是建立在两个socket上面的,说白了现在的连接就是在和服务器的socket连接,形成一个TCP连接。说白了,现在我就是访问客人,先要去找地点(__addr),找不到就把error置为-1,找到,就问一下我可不可以也加入啊(connect)。就是这样。


9. 服务器接收函数(accept)

/* Await a connection on socket FD.
   When a connection arrives, open a new socket to communicate with it,
   set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
   peer and *ADDR_LEN to the address's actual length, and return the
   new socket's descriptor, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int accept (int __fd, __SOCKADDR_ARG __addr,
		   socklen_t *__restrict __addr_len);

这是函数原因以及源码的说明,参数解释一下:

    __fd就是套接字的唯一标识符。

    __addr就是客户端的地址,这个是用来接受客户端的IP地址设置的,一旦存在客户端连接或者队列里面有,则获取它的IP地址。为后面需要使用。

    __addr_len为客户端地址的sizeof。

这个函数有什么用呢?前面说到了,服务器建立了和客户端的连接,那么服务器怎么知道客户端的IP地址呢?这个时候就需要accept来读取客户端的IP地址和一些信息。有什么用,比如你以后需要读取客户端的发送的数据或者需要给客户端发送数据,就需要用到客户端的IP地址。


10. 发送和接受数据(send/recv)

/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

两个函数的原型如上,参数基本一样,就说一个就好:

    __fd为套接字,不做解释了。

    __buf为需要发送或者接受的数据,可以是char*

    __n为需要发送或者接受的长度,为sizeof(buf)

    __flag为标记,一般为0,这个我不是很清楚。

这两个函数就和名字一样,就是用来发送和接受数据的。你可能会很困惑,咦,为什么没有服务器和客户端的IP地址,那它们这样要怎么知道把数据放到哪里,或者从哪里接受数据。前面提到的套接字的功能就在这里,一个socket实际上就是由两个套接字组成(TCP中),这样的话,socket实际上就已经具备了两方的地址和协议。在Unix中,socket实际上可以看成是文件的标识符,你往里面写入东西就像往文件里面写入东西是一样的,这样服务器和客户端双方就可以很好的达成共识,你发给我,我知道,我发给你,你去接受。


11. 定向发送和接受数据(sendto/recvform)

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
		       int __flags, __CONST_SOCKADDR_ARG __addr,
		       socklen_t __addr_len);

/* Read N bytes into BUF through socket FD.
   If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
   the sender, and store the actual size of the address in *ADDR_LEN.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
			 int __flags, __SOCKADDR_ARG __addr,
			 socklen_t *__restrict __addr_len);

参数就不解释了,和上面的一样,只是多了接收方或者发送方的IP地址和端口而已。


12 客户端和服务器样例

好了,上面讲到的API基本上就可以构建一个比较基本的客户端/服务器模型了。

服务器:简单一点吧,就是一台提供服务的高性能机器。

客户端:用户使用的普通计算机,需要服务器为其提供服务。


    上图简单的接受了socket建立简单连接的示意图。

下面是简单服务器/客户端的样例代码:(功能只能建立一对一的连接关系)

服务器:

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <iostream>
#include <errno.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
#define MYPORT 3420
#define BACKLOG 10

int main()
{
    int sockfd = -1;
    int bindres = -1;
    int listenres = -1;

    sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == sockfd){
        perror("sock created");
        exit(-1);
    }

    struct sockaddr_in server;
    memset(&server,0,sizeof(struct sockaddr_in));
    server.sin_addr.s_addr = inet_addr("172.16.27.13");
    server.sin_port = htons(7777);
    server.sin_family = AF_INET;

    bindres = bind(sockfd,(struct sockaddr*)&server,sizeof(server));

    if(-1 == bindres){
        perror("sock bind");
        exit(-1);
    }

    listenres = listen(sockfd,10);
    if(-1 == listenres){
        perror("sock listen");
        exit(-1);
    }

    struct sockaddr_in peerServer;
    int acceptfd = -1;
    socklen_t len = sizeof(peerServer);
    acceptfd = accept(sockfd,(struct sockaddr *)&peerServer,&len);
    if(-1 == acceptfd){
        perror("sock accept");
        exit(-1);
    }

    char recvBuf[1024];
    while(1){
        memset(recvBuf,0,sizeof(recvBuf));
        int recvBytes = recv(acceptfd,recvBuf,sizeof(recvBuf),0);
        fputs(recvBuf,stdout);
        send(acceptfd,recvBuf,recvBytes,0);
    }

    close(acceptfd);
    close(sockfd);

    return 0;
}

客户端代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

int main()
{
    int sockfd = -1;

    sockfd = socket( AF_INET, SOCK_STREAM, 0 );
    if ( -1 == sockfd ) {
        perror( "sock created" );
        exit( -1 );
    }

    struct sockaddr_in server;
    memset(&server,0,sizeof(struct sockaddr_in));
    server.sin_addr.s_addr = inet_addr("172.16.27.13");
    server.sin_port = htons(7777);
    server.sin_family = AF_INET;

    int res = -1;
    res = connect( sockfd, (struct sockaddr*)&server, sizeof( server ) );
    if( -1 == res ){
        perror( "sock connect" );
        exit( -1 );
    }

    char sendBuf[1024] = { 0 };
    char recvBuf[1024] = { 0 };
    while( fgets( sendBuf, sizeof( sendBuf ), stdin ) != NULL ) {
        send( sockfd, sendBuf, sizeof( sendBuf ),0);
        recv( sockfd, recvBuf, sizeof( recvBuf ),0);
        fputs( recvBuf, stdout );
        memset( sendBuf, 0, sizeof( sendBuf ) );
        memset( recvBuf, 0, sizeof( recvBuf ) );
    }

    close( sockfd );

    return 0;
}

上面的代码只能进行一对一的通讯,而且一旦客户端主动关闭连接,那么服务器就会一直陷入死循环。

到此,入门socket编程已经介绍完毕。


(如有错误请多多指教。)


参考链接:

https://blog.csdn.net/ctrl_qun/article/details/52454281

https://www.cnblogs.com/kefeiGame/p/7246942.html

https://blog.csdn.net/ctrl_qun/article/details/52454322



    

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值