网络编程02

网络编程



一、字节序转换接口

第一组:

#include <arpa/inet.h>
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
s 代表 端口号short
l代表 IP地址 long
返回值:
成功返回 要转换的字节序
失败返回 -1
第二组:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//只能用于IPV4转换
int inet_aton(const char *cp, struct in_addr *inp); //主机转网络
in_addr_t inet_addr(const char *cp);//主机转网络
in_addr_t inet_network(const char *cp); //网络转主机
char *inet_ntoa(struct in_addr in);//网络转主机

//填充IP地址的结构体使用
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 本机ip转换为网络ip
//解析ip地址的结构体使用
char *ip = inet_ntoa(client_addr.sin_addr) ;//将网络ip转换为本机ip

第三组:

#include <arpa/inet.h>
//IPV4 和IPV6都兼容的函数
int inet_pton(int af, const char *src, void *dst);

作用:主机转网络
参数:
af:选择哪一种IP地址协议
AF_INET IPV4协议
AF_INET6 IPV6协议
src: 要转换的主机IP地址
dst:转换之后的网络IP地址

const char *inet_ntop(int af, const void *src

作用:网络转主机
参数:
af:选择哪一种IP地址协议:
AF_INET IPV4协议
AF_INET6 IPV6协议
src: 转换之后的主机IP地址
dst:要转换的网络IP地址
size:转换的大小

例子说明:用tcp实现客户端和服务器的互聊
tcp客户端:

#include <stdio.h>
#include <string.h>
#include <unistd.h> //man 2 close
#include <sys/types.h> /* See NOTES */ //man 2 socket
#include <sys/socket.h>
#include <netinet/in.h>  //man 3 inet_addr
#include <arpa/inet.h>
#include <pthread.h>
#include <stdlib.h>

// #define SERVER_IP "192.168.44.2" // windows的ip和端口号
// #define SERVER_PORT 60000

//服务器的ip+port(由服务器决定)
#define SERVER_IP "192.168.44.3"
#define SERVER_PORT 60000

/*
    TCP协议
*/

//接受服务器发送数据的线程
void *recv_server(void *arg)
{
    pthread_detach(pthread_self());

    int ret = 0;
    int socket_fd = *(int*)arg;

    //接收数据
    char buf[1024] = {0};
    while (1)
    {
        bzero(buf, sizeof(buf));
        ret = recv(socket_fd, buf, sizeof(buf), 0);
        if(ret == 0)
        {
            printf("服务器掉线\n");
            exit(0);
        }

        printf("recv ok buf:%s ret:%d\n", buf, ret);
    }
}

int main()
{
    //返回值作用
    int ret = 0;

    // 建立套接字
    int socket_fd;
    // AF_INET--Ipv4  SOCK_STREAM--tcp
    socket_fd = socket(AF_INET, SOCK_STREAM, 0); 
    if (socket_fd < 0)
    {
        perror("socket fail");
        return -1;
    }

    // 填充服务器的地址和端口---->新的结构体
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;                   // 地址族ipv4
    server_addr.sin_port = htons(SERVER_PORT);          // 将本机端口转换为网络端口(小端-->大端) host-to-net-short(端口)
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 将本机IP转换为网络ip(小端-->大端)
    // socklen_t addrlen = sizeof(struct sockaddr_in);

    // 连接服务器---旧的结构体
    // ret = connect(socket_fd,服务器的地址,服务器地址的长度);
    ret = connect(socket_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in));
    if (ret < 0)
    {
        perror("connect fail");
        return -1;
    }
    printf("connect success socket[%d]\n",socket_fd);


    //创建接受的线程
    pthread_t tid;
    ret = pthread_create(&tid,NULL,recv_server,(void *)&socket_fd);
    if(ret != 0)
    {
        printf("pthread_create fail\n");
        return -1;
    }

    char buf[1024] = {0};
    // 与服务器通信---send/recv
    while(1)
    {
        bzero(buf,sizeof(buf));
        scanf("%s",buf);
        // ret = write(socket_fd,buf,strlen(buf)); //可以使用

        /*
            如果服务器掉线,客户端继续发送信息,就会导致进程的退出(系统会给自己发送终止信号)
        */
        ret = send(socket_fd, buf, strlen(buf), 0);
        printf("send ok ret:%d\n", ret);
    }

    // 关闭套接字
    close(socket_fd);

    return 0;
}

tcp服务端:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#define SERVER_IP "192.168.44.31" // ubuntu的ip和端口号
#define SERVER_PORT 60000

/*
    TCP服务器
*/

//发送客户端数据的线程
void *send_client(void *arg)
{
    pthread_detach(pthread_self());

    int ret = 0;
    int socket_client = *(int*)arg;

    //发送数据
    char buf[1024] = {0};
    while(1)
    {
        bzero(buf,sizeof(buf));
        scanf("%s",buf);
        ret = send(socket_client, buf, strlen(buf), 0);
        printf("send ok ret:%d\n", ret);
    }    
}

int main()
{
    int ret = 0; // 作为返回值用

    // 建立套接字
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0)
    {
        perror("socket fail");
        return -1;
    }

    /*
        设置端口复用--防止服务器掉线之后,端口没有阿么快释放
    */
    int optval = 1;
    setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    // 绑定本机(自己)ip和端口---一定要有
    // 填充自己的ip和端口---新的结构体
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;          // 地址族
    server_addr.sin_port = htons(SERVER_PORT); // 本机端口转网络端口
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    ret = bind(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (ret < 0)
    {
        printf("bind fail\n");
        return -1;
    }
    printf("绑定本机成功[%s][%d]\n", SERVER_IP, SERVER_PORT);

    // 监听
    ret = listen(socket_fd, 20);
    if (ret < 0)
    {
        printf("listen fail\n");
        return -1;
    }

    // 接受客户端的链接---accept(阻塞等待客户端的链接)
    int socket_client; // 接收到的客户端的套接字
    struct sockaddr_in client_addr;
    socklen_t addrlen = sizeof(client_addr);
    socket_client = accept(socket_fd, (struct sockaddr *)&client_addr, &addrlen);
    if (socket_client < 0)
    {
        printf("accept fail\n");
        return -1;
    }
    char *ip = inet_ntoa(client_addr.sin_addr); // 网络ip转成本机ip
    int port = ntohs(client_addr.sin_port);     // 网络端口转本机端口
    printf("新的客户端上线:ip[%s] port[%d] socket[%d]\n", ip, port,socket_client);

    /*必须要在accept之后创建线程*/
    pthread_t tid;
    ret = pthread_create(&tid,NULL,send_client,(void *)&socket_client);
    if(ret != 0)
    {
        printf("pthread_create fail\n");
        return -1;
    }   

    // 与客户端进行通信---recv/send
    char buf[1024] = {0};
    while (1)
    {
        bzero(buf, sizeof(buf));
        // ret = read(socket_client,buf,sizeof(buf));
        ret = recv(socket_client, buf, sizeof(buf), 0);
        if (ret == 0)//客户端掉线的时候ret值等于0
        {
            printf("客户端掉线[%s][%d]\n",ip,port);
            break;
        }

        printf("recv ok [%s][%d]buf:%s ret:%d\n", ip, port, buf, ret);
    }

    // 关闭套接字
    close(socket_fd);
    close(socket_client);

    return 0;
}

二、端口号复用

#include <sys/types.h> 
#include <sys/socket.h> 
int setsockopt(int s, int level, int optname, const void * optval, , socklen_toptlen);

函数作用
设置端口号可以复用
参数:
s:套接字文件描述符
level 代表欲设置的网络层, 一般设成 SOL_SOCKET 以存取 socket 层
optname:SO_REUSEADDR 端口号复用
optval 代表欲设置的值, 比如给它一个1,表示使能 端口号复用
optlen 则为 optval 的长度.

//所以设置端口号可以复用,这两条语句放在 绑定bind 之前
int optval = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,&optval, sizeof(optval));

说明(optname参数的部分选项):

在这里插入图片描述

三、TCP通讯时序

下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次挥手

在这里插入图片描述
在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-10,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK1001, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss(Maximum Segment Size,最大报文长度)选项值为1024

建立TCP连接(三次握手)的过程

1.客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1。
客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
2.服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。
服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
3.客户必须再次回应服务器端一个ACK报文,这是报文段3。
客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。
在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused:

$ telnet 192.168.0.200 8080
Trying 192.168.0.200...
telnet: Unable to connect to remote host: Connection refused

数据传输的过程

1.客户端发出段4,包含从序号1001开始的20个字节数据。
2.服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。
3.客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。
在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发

关闭连接(四次挥手)的过程

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
1.客户端发出段7,FIN位表示关闭连接的请求。
2.服务器发出段8,应答客户端的关闭连接请求。
3.服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
4.客户端发出段10,应答服务器的关闭连接请求。
建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止

四、滑动窗口 (TCP流量控制)

现在我们有一个这样的问题:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过“滑动窗口(Sliding Window)”机制解决这一问题。看下图的通讯过程:

在这里插入图片描述
undefined.发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
2.发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
3.接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
4.接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
5.发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
6.接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
7.接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
8.接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
9.接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。
上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。
从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的

五、TCP状态转换

这个图N多人都知道,它排除和定位网络或系统故障时大有帮助,但是怎样牢牢地将这张图刻在脑中呢?那么你就一定要对这张图的每一个状态,及转换的过程有深刻的认识,不能只停留在一知半解之中。下面对这张图的11种状态详细解析一下,以便加强记忆!不过在这之前,先回顾一下TCP建立连接的三次握手过程,以及 关闭连接的四次挥手过程

在这里插入图片描述
CLOSED:表示初始状态。
LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
ESTABLISHED:表示连接已经建立。
FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:
FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。
FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态

六、C/S模型-TCP

下图是基于TCP协议的客户端/服务器程序的一般流程:

在这里插入图片描述
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的: 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段

netstat -an |grep tcp   查看所有tcp连接包括LISTEN状态

思考:为什么TIME_WAIT状态需要经过2MSL才能进入CLOSE状态?
答案:MSL指的是报文在网络中最大生存时间。在客户端发送到服务端的FIN确认包ACK后,这个ACK包有可能到达不了,服务器端如果接收不到ACK包就会重新发送FIN包。所以客户端发送ACK后需要留出2MSL时间(ACK到达服务器器+服务器发送FIN重传包,一来一回)等待确认服务器端确实收到了ACK包。也就是说客户端如果等待2MSL时间也没收到服务器端重传的FIN包,则就可以确认服务器已经收到客户端发送的ACK包

七、UDP协议

1、概念

传输层主要应用的协议模型有两种,一种是TCP协议,另外一种则是UDP协议。TCP协议在网络通信中占主导地位,绝大多数的网络通信借助TCP协议完成数据传输。但UDP也是网络通信中不可或缺的重要通信手段。
相较于TCP而言,UDP通信的形式更像是发短信。不需要在数据传输之前建立、维护连接。只专心获取数据就好。省去了三次握手的过程,通信速度可以大大提高,但与之伴随的通信的稳定性和正确率便得不到保证。因此,我们称UDP为“无连接的不可靠报文传递”。
那么与我们熟知的TCP相比,UDP有哪些优点和不足呢?由于无需创建连接,所以UDP开销较小,数据传输速度快,实时性较强。多用于对实时性要求较高的通信场合,如视频会议、电话会议等。但随之也伴随着数据传输不可靠,传输数据的正确率、传输顺序和流量都得不到控制和保证。所以,通常情况下,使用UDP协议进行数据传输,为保证数据的正确性,我们需要在应用层添加辅助校验协议来弥补UDP的不足,以达到数据可靠传输的目的。
与TCP类似的,UDP也有可能出现缓冲区被填满后,再接收数据时丢包的现象。由于它没有TCP滑动窗口的机制,通常采用如下两种方法解决:
1)服务器应用层设计流量控制,控制发送数据速度。
2)借助setsockopt函数改变接收缓冲区大小。如:

#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

int n = 220x1024;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

2、C/S模型----UDP通信流程

在这里插入图片描述

说明:
本质上来说UDP没有客户端和服务器的说法;只有发送端和接收端。但是UDP也可以写成服务器,它写的服务器要比TCP简单一些

在这里插入图片描述
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现

八、UDP实现编程过程

客户端:

1、建立套接字(选择UDP协议)

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

函数作用:
建立套接字
参数:
domain:你要选择哪一种地址族
PF_INET/AF_INET ------Ipv4 网络协议
PF_INET6/AF_INET6----- Ipv6 网络协议
type: 你要选择哪一种 协议(TCP UDP)
SOCK_STREAM --流式套接字 TCP
SOCK_DGRAM -数据报套接字 UDP
protocol:一般设置成 0
返回值:
成功返回 套接字文件描述符
失败 -1
2、绑定自己的IP地址和端口号

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数:
sockfd:套接字文件描述符
addr:自己的IP地址和端口号
addrlen:结构体的大小
返回值 :
成功则返回 0,
失败返回-1, 错误原因存于 errno 中

struct sockaddr  --旧的结构体
{
    unsigned short sa_family; /地址族/
    char sa_data[14];/14字节的协议地址,包含该socket的IP地址和端口号。/
};
//IPV4结构体      
struct sockaddr_in
{
    short int sin_family; /地址族  IPV4   IPV6/
    unsigned short int sin_port; /端口号/
    struct in_addr sin_addr; /IP地址/
    unsigned char sin_zero[8]; /填充0 以保持与struct sockaddr同样大小/
};
struct in_addr {
    in_addr_t s_addr; /in_addr_t32位的unsigned int,该无符号整数采用大端字节序。/
};

3、直接发送(聊天)

```c
include <sys/socket.h>
ssize_t sendto(int socket, const void *message, size_t length,int flags, const struct sockaddr *dest_addr,socklen_t dest_len);

函数作用
用于UDP中发送数据,注意是UDP
参数:
socket :套接字文件描述符
message:你要发送的数据
length:你要发送的数据大小,注意有多少就发多少 strlen
flags:一般设置成 0
dest_addr:对方的IP地址和端口号
dest_len:结构体的大小
返回值:
成功返回 发送出去的字节数
失败返回 -1
4、关闭

close(socketfd);

udp的发送端的代码:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// ubuntu
#define DEST_IP "192.168.44.31"
#define DEST_PORT 60000

//myself(ubuntu)
#define MY_IP "192.168.44.31"
#define MY_PORT 60002

// windows
//  #define DEST_IP "192.168.44.2"
//  #define DEST_PORT 60000

/*
    udp协议
*/
int main()
{
    int ret;

    // 建立套接字
    int socket_fd;
    // AF_INET--ipv4 SOCK_DGRAM--udp协议
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd < 0)
    {
        perror("socket fail");
        return -1;
    }

    // 绑定本机ip和端口----可有可无
    int optval = 1;
    setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    //  填充自己的ip地址和端口---新的结构体
    struct sockaddr_in my_addr;
    my_addr.sin_family = AF_INET;      // 地址族
    my_addr.sin_port = htons(MY_PORT); // 本机端口->网络端口
    my_addr.sin_addr.s_addr = inet_addr(MY_IP);
    ret = bind(socket_fd, (struct sockaddr *)&my_addr, sizeof(my_addr));

    // 填充目标地址的ip和端口----新结构体
    struct sockaddr_in dest_addr;
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(DEST_PORT);
    dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
    // 给接收端发送数据

    while (1)
    {
        ret = sendto(socket_fd, "hello udp", strlen("hello udp"), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
        printf("sendto ok ret:%d\n", ret);
        sleep(1);
    }

    // 关闭套接字
    close(socket_fd);

    return 0;
}

服务器端:

#include <sys/socket.h>
ssize_t recvfrom(int socket, void * buffer, size_t length,int flags, struct sockaddr * address, socklen_t * address_len);

函数作用
用于UDP中接收数据
参数:
socket :套接字文件描述符
buffer:接收的数据存储在这里
length:接收的数据的大小, 以最大的来接收 sizeof()
flags:一般设置成 0
address: 存储 客户端的IP地址和端口号 ,可以获取到是 谁 给你 发送的
address_len:结构体的大小
返回值:
成功返回 接收到的字节数
失败返回 -1

udp的接收端的代码:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MY_IP "192.168.44.31"
#define MY_PORT 60000

int main(int argc, char **argv)
{

    /*
       ./a.out 192.168.44.2 60001
        argv[1] -->"192.168.44.2
        argv[2] -->60001
        my_addr.sin_port = htons(atoi(argv[2]));
        my_addr.sin_addr.s_addr = inet_addr(argv[1])
    */
    int ret = 0;

    // 创建套接字
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd < 0)
    {
        perror("socket fail");
        return -1;
    }

    int optval = 1;
    setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    // 填充自己的ip地址和端口---新的结构体
    struct sockaddr_in my_addr;
    my_addr.sin_family = AF_INET;      // 地址族
    my_addr.sin_port = htons(MY_PORT); // 本机端口->网络端口
    my_addr.sin_addr.s_addr = inet_addr(MY_IP);

    // 绑定本机IP和端口---一定要有
    ret = bind(socket_fd, (struct sockaddr *)&my_addr, sizeof(my_addr));
    if (ret < 0)
    {
        perror("bind fail");
        return -1;
    }
    printf("b绑定本机成功[%s][%d]\n", MY_IP, MY_PORT);

    //发送端的地址
    struct sockaddr_in send_addr;
    socklen_t address_len = sizeof(struct sockaddr_in);

    // 接受发送端的数据---recvfrom
    char buf[1024] = {0};
    while (1)
    {
        memset(buf,0,sizeof(buf));
        ret = recvfrom(socket_fd, buf, sizeof(buf), 0, (struct sockaddr *)&send_addr, &address_len);

        //解析接收到的ip和端口
        char *ip = inet_ntoa(send_addr.sin_addr);
        int port = ntohs(send_addr.sin_port);

        printf("recvfrom ok [%s][%d]buf:%s ret:%d\n", ip,port,buf, ret);
    }

    // 关闭套接字
    close(socket_fd);

    return 0;
}

# 总结(代码)
客户端代码:

```c
#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MY_ADDR "192.168.1.3"   //绑定本机的ip地址
#define MY_PORT 60001    //绑定本机的端口号

#define DEST_ADDR "192.168.1.3" // 目标地址--->服务器地址
#define DEST_PORT 60000         // 目标端口--->服务器端口

int main(int argc, char **argv)
{
    int ret = 0; // 用作返回值

    // 建立套截字
    int socket_fd;
    // AF_INET--ipv4地址族   SOCK_DGRAM-->udp协议
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd < 0)
    {
        printf("socket fail\n");
        return -1;
    }

    // 设置端口复用
    int optval = 1;
    setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    // 绑定本机IP--可有可无
    struct sockaddr_in my_addr;
    my_addr.sin_family = AF_INET;                 // 地址族
    my_addr.sin_port = htons(MY_PORT);            // 将本机端口转换为网络端口
    my_addr.sin_addr.s_addr = inet_addr(MY_ADDR); // 将本机ip转换为网络ip
    bind(socket_fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)); //固定本机端口

    // 给接收端发送数据(新的结构体)---sendto
    // 填充接收端的(目标端的)地址
    struct sockaddr_in dest_addr;
    dest_addr.sin_family = AF_INET;                   // 地址族
    dest_addr.sin_port = htons(DEST_PORT);            // 将本机端口转换为网络端口
    dest_addr.sin_addr.s_addr = inet_addr(DEST_ADDR); // 将本机ip转换为网络ip
    socklen_t dest_len = sizeof(struct sockaddr_in);

    //同样型结构体强制转换为旧结构体
    ret = sendto(socket_fd, "暑假快结束了", strlen("暑假快结束了"), 0, (struct sockaddr *)&dest_addr, dest_len);
    printf("ret:%d\n", ret);

    // 关闭套截字
    close(socket_fd);

    return 0;
}

服务器代码:

#include  <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#define MY_ADDR "192.168.1.3" // 自己地址--->服务器地址
#define MY_PORT 60000        // 自己端口--->服务器端口

int main(int argc,char **argv)
{
    //建立套截字
    int ret = 0; // 用作返回值

    // 建立套截字
    int socket_fd;
    // AF_INET--ipv4地址族   SOCK_DGRAM-->udp协议
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd < 0)
    {
        printf("socket fail\n");
        return -1;
    }

    //绑定本机IP--要有
    // 设置端口复用
    int optval = 1;
    setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    // 绑定本机IP--可有可无
    struct sockaddr_in my_addr;
    my_addr.sin_family = AF_INET;                 // 地址族
    my_addr.sin_port = htons(MY_PORT);            // 将本机端口转换为网络端口
    my_addr.sin_addr.s_addr = inet_addr(MY_ADDR); // 将本机ip转换为网络ip
    bind(socket_fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)); //固定本机端口

    //接收发送端发送的数据---recvfrom
    char buf[1024] = {0};
    struct sockaddr_in recv_addr;
    socklen_t addrlen = sizeof(struct sockaddr_in);
    //ret = recvfrom(socket_fd,buf,sizeof(buf),0,NULL,NULL); //不用解析发送端的地址
    // printf("buf:%s ret:%d\n",buf,ret);
    ret = recvfrom(socket_fd,buf,sizeof(buf),0,(struct sockaddr *)&recv_addr,&addrlen); 

    //小技巧(解析发送过来的地址和端口号)
    char *ip = inet_ntoa(recv_addr.sin_addr);
    int port = ntohs(recv_addr.sin_port);
    printf("[%s][%d]buf:%s ret:%d\n",ip,port,buf,ret);

    //关闭套截字
    close(socket_fd);

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值