Linux-网络编程学习笔记之tcp

Linux-网络编程学习笔记之tcp

目录

  • -网络编程学习笔记之tcp
    • 目录
    • 前言
    •  预备知识
      •  网络字节序
      • 2 ip 地址转换函数
      • 3 sockaddr 数据结构
    • 网络套接字函数
      • 1 socket函数
      • 2 bind
      • 3 listenconnect函数
      • 4 accept函数
    • 代码示例

前言

最近一段时间从网易游戏辞职在家,有大段时间来研究一下linux下面的东西,之前一直想搞,就是没时间,借着这个机会搞一下吧。ok,闲话不多说,开始正文。


1 预备知识

1.1 网络字节序

由于不同机器存储方式可用大端小端存储,而网络的字节序传输方式采用的是大端方式,为了兼容不同的机器通信,在发送数据前要将数据存储方式改为大端方式,linux下提供了下面几个函数:

  #include 

   uint32_t htonl(uint32_t hostlong);

   uint16_t htons(uint16_t hostshort);

   uint32_t ntohl(uint32_t netlong);

   uint16_t ntohs(uint16_t netshort);

h 表示host,n表示network,l表示32位,s表示16位整数。
32位表示用来转换ip,16位用来转换端口号。

1.2 ip 地址转换函数

下面这两个函数用于将点号分割的ip地址于十进制ip地址之间进行转换

linux下:

include 
include 
include
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);

函数原型:

inet_pton:将“点分十进制” -> “二进制整数”
int inet_pton(int af, const char *src, void *dst);
这个函数转换字符串到网络地址,第一个参数af是地址簇,第二个参数src是来源地址,第三个参数 dst接收转换后的数据。
inet_pton 是inet_addr的扩展,支持的多地址族有下列:
af = AF_INET
src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中。
af = AF_INET6
src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中。
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。

const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);

这个函数转换网络二进制结构到ASCII类型的地址,参数的作用和inet_pton相同,只是多了一个参数socklen_t cnt,他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC。

1.3 sockaddr 数据结构

这个东东就是干三件事情,确定ip,确定port,确定这个socket用哪个协议,ipv4还是ipv6,还是本地的。

下面来看具体结构内容:
在/usr/include/netinet/in.h 中看到sockaddr_in这个结构体
第一个变量就是用来填充所用的协议家族

/* 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)];
  };

第一个成员变量在
usr/include/x86_64-linux-gnu/bits/sockaddr.h

#define __SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

该值其实就是一个整型,可选参数为 
AF_INET、AF_INET6、AF_UNIX 分别代表含义就是ipv4,ipv6,本地

struct sockaddr_in servaddr;

后文中代码会看到详细该结构的使用过程。

2.网络套接字函数

2.1 socket函数

include 
/* See NOTES */
include 
int socket(int domain, int type, int protocol);

domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类
型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的
接受才能进行读取。
SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使
用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数
据包的顺序
protocol:
0 默认协议
返回值:
成功返回一个新的文件描述符,失败返回-1,设置errno

2.2 bind

include 
/* See NOTES */
include 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:
socket文件描述符
addr:
构造出IP地址加端口号
addrlen:
sizeof(addr)长度 是前一个参数的长度
为什么需要这个参数?因为第二个参数,可以接受多种协议的结构体,而他们长度各不相同,所以需要第三个参数来指定结构体长度。
返回值:
成功返回0,失败返回-1, 设置errno

bind函数的作用是讲socket和我们的地址绑定在一起,目的是服务器所用的端口号和ip一般要保持不变,不然客户端连接的时候就找不到对应的进程了。客户端发送数据的时候其实也可以调用bind函数,但没有什么意义,服务器就有很重要的意义,这个端口是所有客户端要知道的,大家都往这个端口发数据,这个端口要是总是变化,客户端连接代码也就要跟着一起变。所以是不合适的。

下面是地址结构体的一般用法

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET; //家族协议代表ipv4
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //ip地址转换,让系统确定本机ip
servaddr.sin_port = htons(8000);    //端口转换成网络字节序

2.3 listen()、connect()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

2.4 accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//返回连接connect_fd

参数sockfd
参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
参数addr
这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
参数len
如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。

如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。

注意:

  accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。

此时我们需要区分两种套接字,

   监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)

   连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。

    一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

    自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。

连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号

3 代码示例

server:
基本流程:
1.创建socket
2.bind socket到指定的ip 端口上
3.在指定端口上监听 listen
4.从监听的端口上获得一个客户端连接socket
5.通过该socket与客户端发送数据

/*
 *服务器端基本流程 
 *
 * 
 */
#include
#include
#include
#include
#include
#include
#include

#define MAXLINE 88
#define SERV_PORT 8000

int main(void)
{

    struct sockaddr_in servaddr,cliaddr;
    socklen_t cliaddr_len;

    int listenfd,connfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    int i,n;

    listenfd = socket(AF_INET,SOCK_STREAM,0);

    bzero(&servaddr,sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    bind(listenfd,(struct sockaddr *) &servaddr,sizeof(servaddr));

    listen(listenfd,20);

    printf("Accepting connections ..... \n");

    while(1)
    {
        cliaddr_len = sizeof(cliaddr);
        connfd = accept(listenfd,(struct sockaddr *) &cliaddr,&cliaddr_len);

        n = read(connfd,buf,MAXLINE);

        printf("received from %s at PROT %d \n",
                inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),
                ntohs(cliaddr.sin_port));

        for(i = 0; i < n; i++)
          buf[i] = toupper(buf[i]);

        write(connfd,buf,n);
        close(connfd);
    }
    return 0;
}

client:

1.创建socket
2.与服务器指定的ip端口连接 connect

/*client.c
 */

#include
#include
#include
#include
#include
#include

#define MAXLINE 88
#define SERV_PORT 8000

int main(int argc,char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd,n;
    char *str;

    if (argc != 2)
    {
        fputs("usage: ./client message\n",stderr);
        exit(1);
    }

    str = argv[1];
    sockfd = socket(AF_INET,SOCK_STREAM,0);

    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    write(sockfd,str,strlen(str));
    n = read(sockfd,buf,MAXLINE);
    printf("Response from server :\n");
    write(STDOUT_FILENO,buf,n);
    close(sockfd);

    return 0;
}

你可能感兴趣的:(linux,c编程)