2015-12-02 11:38:57 Dopamy_BusyMonkey 阅读数 528
  • Linux Socket编程实战第1季第1部分

    课程特点: 1、手把手的实际操作过程; 2、引导学员一步步去思考; 3、网络技术方面初级的一步步进入linux socket编程的世界; 本课程是linux socket编程的一小部分,从无名套接口开始, 然后逐步深入,这应该是很多课程所没有的。 以通俗的比照讲清楚一些概念,更多的是如何一步步通过代码去实现,并辅之以一些小的项目来更好的 理解linux socket编程的技巧和方法。

    1891 人正在学习 去看看 熊健

服务器端:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define DEFAULT_PORT 8000
#define MAXLINE 4096

int main(int argc, char** argv)
{
    int    socket_fd, connect_fd;
    struct sockaddr_in     servaddr;
    char    buff[4096];
    int     n;
    //初始化Socket
    if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 )
    {
        printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }
    //初始化
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY。
    servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT

    //将本地地址绑定到所创建的套接字上
    if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }
    //开始监听是否有客户端连接
    if( listen(socket_fd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }
    printf("======waiting for client's request======\n");
    while(1)
    {
        //阻塞直到有客户端连接,不然多浪费CPU资源。
        if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1)
        {
            printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
            continue;
        }
        //接受客户端传过来的数据
        n = recv(connect_fd, buff, MAXLINE, 0);
        //向客户端发送回应数据
        if(!fork())
        {
            if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)
                perror("send error");
            close(connect_fd);
            exit(0);
        }
        buff[n] = '\0';
        printf("recv msg from client: %s\n", buff);
        close(connect_fd);
    }
    close(socket_fd);
}

客户端:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLINE 4096

int main(int argc, char** argv)
{
    int    sockfd,rec_len;
    char    sendline[4096];
    char    buf[MAXLINE];
    struct sockaddr_in    servaddr;

    if( argc != 2)
    {
        printf("usage: ./client <ipaddress>\n");
        exit(0);
    }

    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8000);

    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
        printf("inet_pton error for %s\n",argv[1]);
        exit(0);
    }

    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
    {
        printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    printf("send msg to server: \n");
    fgets(sendline, 4096, stdin);
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)
    {
        printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }
    if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1)
    {
        perror("recv error");
        exit(1);
    }
    buf[rec_len]  = '\0';
    printf("Received : %s ",buf);
    close(sockfd);
    exit(0);
}

分别用两个终端运行。

1、int socket(int domain, int type, int protocol);  //成功返回套接字描述符,出错返回-1。

domain   确定通信的特性,包括地址格式。


type   确定套接字的类型,进一步确定通信特征。


protocol通常为0,表示按给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个系协议时,可以使用protocol选择一个特定协议。


2、int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)返回:0──成功, -1──失败

sockfd   指定地址与哪个套接字绑定,这是一个由之前的socket函数调用返回的套接字。调用bind的函数之后,该套接字与一个相应的地址关联,发送到这个地址的数据可以通过这个套接字来读取与使用。

addr   指定地址。这是一个地址结构,并且是一个已经经过填写的有效的地址结构。调用bind之后这个地址与参数sockfd指定的套接字关联,从而实现上面所说的效果。

addrlen   正如大多数socket接口一样,内核不关心地址结构,当它复制或传递地址给驱动的时候,它依据这个值来确定需要复制多少数据。这已经成为socket接口中最常见的参数之一了。

bind   函数并不是总是需要调用的,只有用户进程想与一个具体的地址或端口相关联的时候才需要调用这个函数。如果用户进程没有这个需要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择,而不需要调用bind的函数,同时也避免不必要的复杂度。在一般情况下,对于服务器进程问题需要调用bind函数,对于客户进程则不需要调用bind函数。


3、int listen(int sockfd, int backlog)返回:0──成功, -1──失败

sockfd   参数用来主动监听套接字,用来接收其他连接

backlog   参数并不是用来限制程序的最大连接数的。而是TCP模块允许的已完成三次握手过程(TCP模块完成)但还没来得及被应用程序accept的最大链接数。举例来说假说你的服务器很忙或干脆暂停了,这时如果有四个TCP客户端尝试TCP链接过来,而你listen的第二个参数为3,则底层TCP模块会依次完成前面3个客户端的握手过程,并把其放入一个缓存区中等待服务器应用程序的accept(此时这三个客户程序的connect会调用成功表示链接已建立)。对于第四个链客户端,TCP模块什么也不做,也不会返回icmp数据包。实际上第四个链接已被TCP模块丢弃。假如此时服务器还在暂停之中,但客户端的connect已成功返回则客户端会认为可以向服务器发送数据了,这此数据会被服务器端的TCP模块缓存下来,待服务器程序从暂停中恢复过来时读取。


4、int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd   套接字描述符,该套接口在 listen() 后监听连接。

addr   (可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。

addrlen   (可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。


5、ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);

sockfd   接收端套接字描述符

buff   用来存放recv函数接收到的数据的缓冲区

nbytes   指明buff的长度

flags   一般置为0



6、ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);

sockfd   指定发送端套接字描述符。

buff   存放要发送数据的缓冲区

nbytes   实际要改善的数据的字节数

flags    一般设置为0


7、close(socket);


8、int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); 

sockfd   指定连接套接字描述符。

serv_addr   连接地址

nbytes   地址长度

待检测:::((首先问问自己客户端调用connect是在什么时候返回,肯定是在服务器accept后返回,因为TCP/IP协议是面向连接。自己得出来答案,然后再去问问身边的人,基本上都是这个答案。但是很可惜结果不是这样子,因为TCP/IP协议是经过三次握手的,要是服务器在listen,首先是客户端向通过connect向服务器发送request,然后服务器发送ACK,当客服端接受到ACK,connect就返回,这时候连接已经建立了,但是只是客户端可以给服务器发送数据,当服务器accept后,就是可以进行双方通讯了。不相信的话,可以自己写代码测试一下,就是在服务器listen后sleep一段时间,就是会发现客户端那里connect马上返回,而服务器sleep时间还没有过。))




2020-02-16 20:58:28 huwei039 阅读数 71
  • Linux Socket编程实战第1季第1部分

    课程特点: 1、手把手的实际操作过程; 2、引导学员一步步去思考; 3、网络技术方面初级的一步步进入linux socket编程的世界; 本课程是linux socket编程的一小部分,从无名套接口开始, 然后逐步深入,这应该是很多课程所没有的。 以通俗的比照讲清楚一些概念,更多的是如何一步步通过代码去实现,并辅之以一些小的项目来更好的 理解linux socket编程的技巧和方法。

    1891 人正在学习 去看看 熊健

Linux Socket 网络编程辅助函数 {ignore}

网络字节序的调整

由于CPU采用的架构或者配置的不同,不同的CPU中数据存储的大端模式、小端模式是不同的,特定的主机上使用的字节序被称为主机字节序。而网络中IP、端口号等信息必须以统一的模式在网络中传输,故称此为网络字节序,网络字节序是大端模式的。IP地址、端口号信息,在存储到struct sockaddr 之前需要转化为网络字节序。当主机字节序与网络字节序相同时,转换函数原样返回传入的参数。

#include <netinet/in.h>

uint16_t htons(uint16_t host_uint16);   //host to net short 将16位主机字符顺序转换成网络字符顺序,常用于端口号的转换
uint32_t htonl(uint32_t host_uint32);   //host to net long 将32位主机字符顺序转换成网络字符顺序,常用于IP地址的转换
uint16_t ntohs(uint16_t net_uint16);    //net to host short 将16位网络字符顺序转换成主机字符顺序,常用于端口号的转换
uint32_t ntohl(uint32_t net_uint32);    //net to host long 将32位网络字符顺序转换成主机字符顺序,常用于IP地址的转换

IPv4地址的二进制形式与点分十进制表示间的转换

注意:下述函数,仅能用于IPv4型地址类型的转换。下述函数均属于已经过时的函数,最新的标准中已被废弃。新函数可参考下节内容。
点分十进制表示"192.168.1.80"是用一段字符串表示,二进制形势表示是以一个 uint32_t 表示一个IP地址。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long int inet_addr(const char *cp);            //将cp指向的字符串内容转化为**网络字节序**的二进制IP值  例: int ip = inet_addr("192.168.1.80");
int inet_aton(const char * cp,struct in_addr *addr);    //将cp指向的字符串内容转化为**网络字节序**的二进制IP值,并将地址存储于addr的结构体中 in_addr结构参考我的上一篇博客
char * inet_ntoa(struct in_addr addr);                  //将addr中的IPv4地址转化为点分十进制字符串形式,并返回字符串的指针。

注意:
1. inet_addr()、inet_aton()转化得到的IP值都是网络字节序的。
2. inet_ntoa()转化得到的字符串存储在函数中的一个静态地址中,转化后需要立刻使用或取出,因为后续的调用会覆盖该值。

IPv4 与 IPv6兼容的二进制形式与点分十进制形式的转换

注意:下述函数,即可以实现IPv4型的二进制IP地址转换为点分十进制字符串,也可以实现IPv6的地址转换。这是最新的函数标准。

#include <arpa/inet.h>

/*
 *  @Description: 将src_str中的点分十进制IP地址转化为二进制IP,存储到addrptr中。
 *  @Para       : int domain IP地址类型,主要有如下
 *                      AF_INET(IPv4)
 *                      AF_INET6(IPv6)
 *                const char * src_str  点分十进制的IP地址字符串
 *                void * addrptr        存储转化为二进制IP地址值,如果domain为IPv4型则
 *                                      该地址结构为struct in_addr,IPv6为 struct in6_addr。
 * 
 *  @return     : 成功返回1,src_str不合法时返回0,失败返回-1,失败的错误码在errno中
**/
int inet_pton(int domain, const char * src_str, void *addrptr);

/*
 *  @Description: 将二进制IP转化为src_str中的点分十进制,存储到dst_str中。
 *  @Para       : int domain IP地址类型,主要有如下
 *                      AF_INET(IPv4)
 *                      AF_INET6(IPv6)
 *                void * addrptr    存储转化为二进制IP地址值,如果domain为IPv4型则该
 *                                  地址结构为struct in_addr,IPv6为 struct in6_addr。
 *                char * src_str    点分十进制的IP地址字符串存储缓存区
 *                size_t len        缓冲区src_str的大小
 * 
 *  @return     : 成功返回dst_str,失败返回NULL,失败的错误码在errno中
**/
const char * inet_ntop(int domain, const void * addrptr, char * dst_str, size_t len);

Socket的配置选项 setsockopt() 与 getsockopt()

Socket选项能够影响到Socket操作的多个功能,具体功能可参考optname参数的设置。

#include<sys/types.h>
#include<sys/socket.h>

/*
 *  @Description: 设置socket配置选项
 *  @Para       : int sockfd    要设置的socket文件描述符
 *                int level     代表欲设置的网络层,一般设成 SOL_SOCKET 以存取socket层
 *                int optname   代表欲设置的选项,有下列几种数值:
 *                      SO_DEBUG 打开或关闭排错模式
 *                      SO_REUSEADDR 允许在bind()过程中本地地址可重复使用   注意:该特性的重要性在上篇博客中强调过
 *                      SO_TYPE 返回socket形态。
 *                      SO_ERROR 返回socket已发生的错误原因
 *                      SO_DONTROUTE 送出的数据包不要利用路由设备来传输。
 *                      SO_BROADCAST 使用广播方式传送
 *                      SO_SNDBUF 设置送出的暂存区大小
 *                      SO_RCVBUF 设置接收的暂存区大小
 *                      SO_KEEPALIVE 定期确定连线是否已终止。
 *                      SO_OOBINLINE 当接收到OOB 数据时会马上送至标准输入设备
 *                      SO_LINGER 确保数据安全且可靠的传送出去。
 *                      SO_RCVTIMEO 接收数据阻塞进程(线程)超时时间设置
 *                      SO_SNDTIMEO 发送数据阻塞进程(线程)超时时间设置
 *                const void * optval   代表欲设置值的结构体指针
 *                socklen_t optlen      为optval的数据长度
 * 
 *  @return     : 成功返回0,失败返回-1,失败的错误码在errno中
**/
int setsockopt(int sockfd, int level, int optname, const void * optval, socklen_t optlen);

    /*例: 设置SO_REUSEADDR,允许在bind()过程中本地地址可重复使用*/
    int optval = 1;
    if(setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
        printf("socket SO_REUSEADDR option set failed!\r\n");


    /*例: 接收超时时间设置(accept()阻塞、recv()阻塞均受该配置影响)*/
    struct timeval timeout={5,0};   //接收的timeout延时为5s
    //服务端接收客户端的数据超时时间为5s
    int ret = setsockopt(ClientNow -> client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout,sizeof(timeout));
    if(ret != 0)    //配置失败
    {
        DeleteClient(ClientNow);
        break;
    }

/*
 *  @Description: 获取socket配置选项
 *  @Para       : int sockfd    要获取参数的socket文件描述符
 *                int level     代表欲获取的网络层,一般设成 SOL_SOCKET 以存取socket层
 *                int optname   代表欲获取的选项,具体数值参考setsockopt():
 *                const void * optval   代表存储要获取值的结构体的指针
 *                socklen_t * optlen    调用函数前需要设置optlen指向的内容为optval结构的缓存区长度,
 *                                      调用函数后,该参数为写入到optval中的数据长度。
 *                                      (注意:optlen参数既是一个输入参数也是一个输出参数)
 * 
 *  @return     : 成功返回0,失败返回-1,失败的错误码在errno中
**/
int getsockopt(int s,int level,int optname,void* optval,socklen_t* optlen);

    //例:获取socket的TCP类型或者UDP类型,类型最终返回到optval中,
    //TCP类型返回SOCK_STRREAM, UDP类型返回SOCK_DGRAM
    int optval;
    socklen_t optlen;
    optlen = sizeof(optval);
    if(getsockopt(server_fd, SOL_SOCKET, SO_TYPE, &optval, &optlen) == -1)
        errExit("getsockopt");

获取Socket的地址参数 getsockname() 与 getpeername()

getsockname()用于获取本地的IP地址参数,getpeername()用于获取远端地址参数。有时地址参数使用的是临时参数,并不是用户预先设置的,如果想获取临时的地址参数,则可采用该函数。

#include <sys/socket.h>

/*
 *  @Description: 获取本地socket的地址参数
 *  @Para       : int sockfd                要获取参数的socket文件描述符
 *                struct sockaddr * addr    存储获取地址的结构体指针
 *                socklen_t * addrlen       调用函数前需要设置addrlen指向的内容为addr结构的缓存区长度,
 *                                          调用函数后,该参数为写入到addr中的数据长度。
 *                                          (注意:addrlen参数既是一个输入参数也是一个输出参数)
 * 
 *  @return     : 成功返回0,失败返回-1,失败的错误码在errno中
**/
int getsockname(int sockfd, struct sockaddr * addr, socklen_t * addrlen);
// getpeername()函数参数含义与getsockname()参数含义相同
int getpeername(int sockfd, struct sockaddr * addr, socklen_t * addrlen);

Socket的文件传输 sendfile() 与 readline()

在Web服务器或者文件服务器中,往往需要将磁盘上的文件内容不做修改的通过socket传输出去。通过常规的额recv() send()方法也能够实现该传输,但是普通方法传输磁盘文件需要在内核空间和用户空间来回拷贝数据,效率不高。当使用sendfile()函数传输,文件内容仅在内核空间传输,直接在内核缓存区通过socket发送出去,从而提高传输效率。
注意:sendfile()仅能用于将文件传输到socket上,反过来则不行,也不能在两个socket间传送数据。

#include <sys/sendfile.h>

/*
 *  @Description: 发送文件到socket上
 *  @Para       : int out_fd        必须指向一个socket描述符
 *                int in_fd         要传送的文件描述符
 *                off_t * offset    起始文件的偏移量,即从in_fd指向的文件的这个位置开始传送数据,从文件首部开始可设置为NULL。
 *                size_t count      要传送的字节数
 * 
 *  @return     : 成功返回已经传送的字节数,失败返回-1,失败的错误码在errno中
**/
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

如果socket上传输的是以换行符分隔的文本,那么使用readline()读取一行是比较便捷的。

#include "read_line.h"

/*
 *  @Description: 从socket描述符中读取字节直到碰到换行符为止
 *  @Para       : int fd            要读取的socket描述符
 *                void * buffer     读取的字节缓存区,返回的字符串总以null结尾。
 *                size_t n          要读取的字节数,参数为n,则最多能读取n-1个字节,并返回(n-1)。
 *                                  结尾的null字节不会记为返回的字节数。
 * 
 *  @return     : 成功返回已经保存到buffer的字节数,读取到EOF则返回0,失败返回-1,失败的错误码在errno中
**/
ssize_t readline(int fd, void * buffer, size_t n);

注意:如果实际一行的字节数大于缓冲区的个数(n-1),则readline()会丢弃多余的字节(包括换行符)。可以通过buffer中结尾null字节前是否是一个换行符来判断readline()是否丢弃字节

选择性关闭连接shutdown()

如果使用close()关闭socket连接,通道的两端都会关闭。shutdown()提供了只关闭一端的功能。

#include <sys/socket.h>

/*
 *  @Description: 根据how参数关闭连接的一端或者两端
 *  @Para       : int sockfd    要关闭的socket连接
 *                int how       关闭方式
 *                      SHUT_RD     关闭连接的读端
 *                      SHUT_WR     关闭连接的写端
 *                      SHUT_RDWR   关闭连接的读端和写端
 * 
 *  @return     : 成功返回0,失败返回-1,失败的错误码在errno中
**/
int shutdown(int sockfd, int how);
2015-11-30 18:51:57 ReturnZero__ 阅读数 430
  • Linux Socket编程实战第1季第1部分

    课程特点: 1、手把手的实际操作过程; 2、引导学员一步步去思考; 3、网络技术方面初级的一步步进入linux socket编程的世界; 本课程是linux socket编程的一小部分,从无名套接口开始, 然后逐步深入,这应该是很多课程所没有的。 以通俗的比照讲清楚一些概念,更多的是如何一步步通过代码去实现,并辅之以一些小的项目来更好的 理解linux socket编程的技巧和方法。

    1891 人正在学习 去看看 熊健


Linux socket 学习

TCP server

服务端建立步骤一般为:socket->bind->listen->accept

#include <stdio.h>      /* stdin, stdout, stderr */
#include <string.h>     /* memset, strerror */
#include <sys/types.h>  /* socket, bind, listen, send */
#include <sys/socket.h> /* socket, bind, listen, send */
#include <netinet/in.h> /* sin_family, sin_port, sin_addr (man in.h) */
#include <arpa/inet.h>  /* inet_pton, htons (man inet.h) */
#include <errno.h>      /* errno */

#define BUFSIZE 1024
#define IP      "127.0.0.1"
#define PORT    8888
#define LISTENQ 5

/* 
 * gcc tcp_server.c -o server 
 * $ ./server
 * */
int main(int argc, char *argv[])
{
    int    srv_sockfd;
    int    cli_sockfd;
    size_t len;
    size_t sin_size;
    struct sockaddr_in srv_addr;
    struct sockaddr_in cli_addr;
    char   buf[BUFSIZE];

    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    inet_pton(AF_INET, IP, &srv_addr.sin_addr);
    srv_addr.sin_port = htons(PORT);

    /* TCP: socket套接字 */
    if ((srv_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        fprintf(stderr, "create socket fail, %s.\n", strerror(errno));
        return 1;
    }

    /* bind地址 */
    if (bind(srv_sockfd, (struct sockaddr*)&srv_addr, 
        sizeof(struct sockaddr)) < 0) 
    {
        fprintf(stderr, "create bind fail, %s.\n", strerror(errno));
        return 1;
    }

    /* listen接口 */
    listen(srv_sockfd, LISTENQ);

    sin_size = sizeof(struct sockaddr_in);
    /* 等待客户端连接请求 */
    if ((cli_sockfd = accept(srv_sockfd, (struct sockaddr*)&cli_addr, 
        &sin_size)) < 0)
    {
        fprintf(stderr, "create accept fail, %s.\n", strerror(errno));
        return 1;
    }

    fprintf(stdout, "accept client %s\n", inet_ntoa(cli_addr.sin_addr));
    sprintf(buf, "Hello client, ip:%s\n", inet_ntoa(cli_addr.sin_addr));
    len = send(cli_sockfd, buf, strlen(buf), 0);

    /* 接受客户端数据 */
    memset(&buf, 0, sizeof(char) * BUFSIZE);
    while((len = recv(cli_sockfd, buf, BUFSIZE, 0)) > 0) {
        buf[len] = '\0';
        fprintf(stdout, "%s\n", buf);
        if (send(cli_sockfd, buf, len, 0) < 0) { /* 原样发回 */
            fprintf(stderr, "create accept fail, %s.\n", strerror(errno));
            return 1;
        }
    }
    close(cli_sockfd);
    close(srv_sockfd);
    return 0;
}

TCP client

客户端建立步骤一般为:socket->connect

#include <stdio.h>      /* stdin, stdout, stderr */
#include <string.h>     /* memset, strerror */
#include <sys/types.h>  /* socket, bind, listen, send */
#include <sys/socket.h> /* socket, bind, listen, send */
#include <netinet/in.h> /* sin_family, sin_port, sin_addr (man in.h) */
#include <arpa/inet.h>  /* inet_pton, htons (man inet.h) */
#include <errno.h>      /* errno */

#define BUFSIZE 1024
#define IP      "127.0.0.1"
#define PORT    8888
#define LISTENQ 5

/* 
 * gcc tcp_client.c -o client 
 * $ ./client
 * */
int main(int argc, char *argv[])
{
    int    cli_sockfd;
    size_t len;
    struct sockaddr_in srv_addr;
    char   buf[BUFSIZE];

    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    inet_pton(AF_INET, IP, &srv_addr.sin_addr);
    srv_addr.sin_port = htons(PORT);

    /* TCP: socket套接字 */
    if ((cli_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        fprintf(stderr, "create socket fail, %s.\n", strerror(errno));
        return 1;
    }

    /* connect server */
    if (connect(cli_sockfd, (struct sockaddr*)&srv_addr, 
        sizeof(struct sockaddr)) < 0)
    {
        fprintf(stderr, "create connect fail, %s.\n", strerror(errno));
        return 1;
    }

    fprintf(stdout, "connected to server\n");
    len = recv(cli_sockfd, buf, BUFSIZE, 0);
    buf[len] = '\0';
    fprintf(stdout, "%s\n", buf);

    while(1) {
        fprintf(stdout, "Enter string to send:");
        scanf("%s", buf);
        len = send(cli_sockfd, buf, strlen(buf), 0);
        len = recv(cli_sockfd, buf, BUFSIZE, 0);
        buf[len] = '\0';
        fprintf(stdout, "received:%s\n", buf);
    }
    close(cli_sockfd);
    return 0;
}

参考资料

[1] http://blog.csdn.net/piaojun_pj/article/details/5920888
[2] http://blog.sina.com.cn/s/blog_50571b1f010082pg.html

2015-06-17 18:44:19 yekongxiadebeijixing 阅读数 307
  • Linux Socket编程实战第1季第1部分

    课程特点: 1、手把手的实际操作过程; 2、引导学员一步步去思考; 3、网络技术方面初级的一步步进入linux socket编程的世界; 本课程是linux socket编程的一小部分,从无名套接口开始, 然后逐步深入,这应该是很多课程所没有的。 以通俗的比照讲清楚一些概念,更多的是如何一步步通过代码去实现,并辅之以一些小的项目来更好的 理解linux socket编程的技巧和方法。

    1891 人正在学习 去看看 熊健

Linux socket编程

好多工作,唧唧歪歪的事情真多,本来打算今天早上写的,现在好了可以回家了(下班时间)。

主机字节序 网络字节序

主机字节序
主机字节序就是多字节数据中字节在内存中的存储顺序,有大端字节序和小端字节序。
大端/小端的语义就是多字节值的哪一端存储在该值的起始位置,大端存储在起始位置就是大端字节序(Big-Endian),小端存储在起始位置就是小端字节序(Little-Endian)。
如4Byte int 0x12345678小端存储方式:
31 -24 23-16 15-8 7-0
|0x12|0x34|0x56|0x78|
大端存储方式:
31 -24 23-16 15-8 7-0
|0x78|0x56|0x34|0x12|

网络字节序
网络中数据流的传输方式为字节流,传输一个多字节值时先传输哪个字节呢?
网络字节序定义,发送端发送的第一个字节为多字节值的高位,接收端接收到的第一个字节也作为多字节值的高位。但是发送端在发送数据时,发送的第一个字节为该数据在内存中起始位置对应的字节;同样接收端在接收到第一个字节时存储到该数据起始位置。也就是说不管是发送端还是接收端,数据在内存中的存储方式都应该和网络字节序一样为大端字节序。
但是不同的主机有不同的字节序,所以在网络应用程序中发送端发送数据前可以将主机序转换为网络字节序,接收端接收到数据后由网络字节序转换为主机序。
可以使用下面的库函数进行主机序与网络字节序转换:

#include <arpa/inet.h>
// h:host, n:network, l:long, s:short
// 如果字节序为小端字节序将转换为大端字节序,如果为大端字节序将不进行转换而直接返回数据(发送端/接收端都一样)
uint32_t htonl(uint32_t hostlong);
uint16_t htons(unit16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohl(uint16_t netshort);

什么是socket

在说socket之前,先了解下在本地以及网络中如何标识一个唯一的进程。
在本地中通过PID唯一标识一个进程;但是在网络中需要通过IP地址唯一标识一台主机,在通过协议+端口唯一标识主机中进程。(IP地址:网络层;协议+端口:传输层/应用层)
socket是网络中进程通信的接口,由于UNIX的思想为一切皆文件,socket可以理解为一种特殊的文件,其操作模式为open->read/wirte->close模式的特殊实现方式。

sockaddr sockaddr_in/sockaddr_in6 sockaddr_un

我们知道网络中标识唯一进程需要IP地址、协议、及端口号,而网络地址就是用来保存这些信息的,根据不同的协议有不同的地址结构体。

// IPv4
// sockaddr 通用套接字地址, 一般作为参数传递,如传入bind()
#include socket.h
typedef unsigned short int sa_family_t;
struct sockaddr {
    sa_family_t sa_family; // address family
    char sa_date[14]; // address data
}

// sockaddr_in internet环境套接字地址,一般用于初始化网络地址,然后转换为sockaddr传递给函数使用(sockaddr_in/sockaddr大小相同,可以相互转换)
#incldue netinet/in.h
typedef unsigned short int in_port_t;
struct sockaddr_in {
    sa_family_t sin_family; // address family,AF_INET
    in_port_t sin_port; // port munber
    struct in_addr sin_addr; // internet address
    unsigned char sin_zero[8]; // pad to size of sockaddr
}
typedef unsigned int in_addr_t;
struct in_addr {
    in_addr_t s_addr;
}
// UNIX域
#defien UNIX_PATH_MAX 108
struct sockaddr_un {
    sa_family_t sun_family; // address family, AF_LOCAL/AF_UNIX
    char sun_path[UNIX_PATH_MAX]; // pathname
}
// IPv6
struct sockaddr_in6 {
    sa_family_t sin6_family; // address family, AF_INET6
    in_port_t sin6_port; // port number
    uint32_t sin6_flowinfo; // IPv6 flow info
    struct in6_addr sin6_addr; // IPv6 address
    uint32_t sin6_scope_id; // Scope ID
}
struct in6_addr {
    unsigned char s6_addr[16];
}
// 地址转换函数
// IPv4 and IPv6
int inet_aton(const char *cp, struct in_addr *inp); // 点分十进制IP到二进制格式(网络字节序)
in_addr inet_addr(const char *cp); // 与inet_aton()相同,但不能传入255.255.255.255,否则返回INADDR_NONE
char *inet_ntoa(struct in_addr in); // 二进制格式(网络字节序)转换为点分十进制IP
// IPv6
int inet_pton(int af, const char *src, void *dest); // af:AF_INET/AF_INET6, 点分十进制到二进制格式(网络字节序,根据af转换为不同的格式4字节in_addr/16字节in6_addr,然后给dest)
const char *inet_ntop(int af, const void *src, char *det, socklen_t size); // 二进制格式到点分十进制,zise为det的长度,避免溢出

socket函数

1、int socket(int domain, int type, int protocol);
socket()函数用于打开一个socket,返回socket描述符,这个socket描述符用于后续的操作。如读写。
domain:协议域/协议簇,AF:Address Family,地址簇,BSD使用;PF:Protocol Family协议簇,POSIX使用。(Linux中使用AF或者PF前缀是一样的)
AF_INET :IPv4地址(32bit地址+16端口号);
AF_INET6:IPv6地址(128bit地址+16端口号);
AF_UNIX/AP_LOCAL:UNIX域(使用路径名作为地址);
AF_PACKET:底层数据包接口;
type:socket类型
SOCK_STRAEM:面向连接的流套接字,TCP协议;
SOCK_DGRAM:面向非连接的用户数据报套接字,UDP协议;
SOCK_RAW:原始套接字;
protocol:协议
IPPROTO_TCP:TCP协议;
IPPROTO_UDP:UDP协议;
IPPROTO_ICMP:ICMP协议;
ETH_P_IP:IP协议;
ETH_P_ARP:ARP协议;
ETH_P_RARP:RARP协议;
ETH_P_ALL:IP/ARP/RARP协议;

使用socket函数的三种形式
发送/接收的数据为应用层数据:
socket(AF_INET, SOCK_STREAM|SOCK_DGRAM, 0); // protocol为0,会根据type自动选择协议类型
原始套接字1:发送/接收的数据从网络层开始
socket(AF_INET, SOCK_ARW, IPPROTO_TCP | IPPROTO_UDP | IPPROTO_ICMP);
接收时,接收到包含IP报头的数据包;
发送时,只能发送包含TCP、UDP、或其他协议的报头,IP报头自动封装,设置socket的IP_HDRINCL可以手动发送IP报头(以太网报头自动封装);
原始套接字2:发送/接收的数据从链路层开始
socket(AF_PACKET, SOCK_RAW/SOCK_DGRAM, htonl(ETH_P_IP | ETH_P_ARP | ETH_P_RARP | ETH_P_ALL));
灰常强大的套接字,发送接收以太网数据包,只有root才可以创建这种套接字;
type:SOCK_RAW与SOCK_DGRAM的区别在于,SOCK_DGRAM发送接收的数据包不包括以太网数据报头;

// 将套接字描述符sockfd与sockaddr地址绑定
// 服务器启动时绑定一个地址(IP+端口)用于提供服务;客户端不需要绑定,在connect时由系统自动分配
2、int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 监听套接字sockfd,backlog为连接队列最大数量
3、int listen(int sockfd, int backlog);

// 客户端与TCP服务器建立连接
// sockfd为客户端套接字描述符,sockaddr为服务器地址,socklen为地址长度
4、int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// TCP服务器等待来自客户端的连接(会阻塞直到客户端产生连接),建立连接后就可以进行I/O操作(读写操作)
// sockfd为服务器监听的套接字描述符,addr指针保存客户端地址,addlen地址长度,返回内核生成的连接描述符(表示与客户端的连接)
5、int accept(int sockfd, const struct sockaddr *addr, socklen_t *addrlen);

// 当服务器与客户端建立连接后就可以进行I/O操作了
// 将buf的nbytes字节内容写入fd文件描述符,返回成功写入的字节数,error返回-1(errno)
6、ssize_t write(int fd, const void*buf, size_t nbytes);
// 网络程序中分两种情况处理:
返回值大于0,写入部分或者全部数字;
返回值小于0,error(errno=EINTR,写时中断;errno=EPIPE,网络连接中断,对方已关闭连接);

//从fd读取内容到buf(nbyte返回限定buf的大小防溢出),返回读到的字节数;返回0,读到EOF;返回值小于0,error(errno=EINTR,读时中断;errno=ECONNRES,网络连接中断)
7、ssize_t read(int fd, void *buf, size_t nbyte);

// recv/send与read/write相似,多了控制选项flag
8、ssize_t recv(int sockfd, void *buf, size_t len, int flags);
9、ssize_t send(int sockfd, void *buf, size_t len, int flags);
flags选项:
MSG_DONTROUTE:不查找路由表(send()函数使用,告诉IP协议,目的主机在本地网络上);
MSG_OOB:发送/接收外带数据;
MSG_PEEK:读取数据,并不从缓冲区移走数据(一般为多进程读取);
MSG_WAITALL:等待所有数据(recv函数使用,一直阻塞直到等待的条件满足,如指定读取一定的数据);

// recvfrom/sendto与recv/send相似,多了发送/接收的地址信息
10、ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // 保存信息来源地址src_addr
11、ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // 发送的目的地址dest_addr

// 发送/接收的地址信息和数据信息保存在msghdr
12、ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
13、ssize_t recvmsg(int sockfd, const struct msghdr *msg, int flags);

// 设置/获取套接字的选项
13、int setsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
14、int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
// level:控制套接字层次,SOL_SOCKET通用套接字、IPPROTO_IP(IP选项)、IPPROTO_TCP(TCP选项);
// optname:选项名称,如IP_HDRINCL(在数据报包中加入IP头部);
// optvalue:选项值,根据选项名称进行类型转换;
// optlen:optval的长度;

服务器模型

服务器模型是网络编程的思想,现在常用的服务器模型有循环服务器模式和并发服务器模式。
循环服务器:在同一时刻,服务器只能响应一个客户端请求;
并发服务器:在同一时刻,服务器可以响应多个客户端请求;
1、UDP循环服务器
客户端请求、处理、返回处理结果;由于UDP是面向非连接的,所以没有个一个客户端可以一直占据着服务器,即服务器一般可以满足每个客户端的请求。
2、TCP循环服务器
客户端请求、处理、完成所有请求后断开连接;由于TCP是面向连接的,所以服务器需要处理完一个客户端的所有请求并断开连接后才可以继续处理下一个客户端的请求。
3、TCP并发服务器
服务器对于客户端的每一个请求不直接处理,而是创建一个子进程处理;解决了TCP循环服务器独占问题,但是创建的子进程需要消耗大量的系统资源。
4、UDP并发服务器
与TCP并发服务器类似,一般不常用。
5、并发服务器:多路I/O复用
解决了TCP循环服务器独占问题,又避免了TCP并发服务器消耗大量系统资源的问题
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// nfds:最大文件描述符加1;
// readfds/writefds/exceptfds:读/写/异常文件描述符集;
// timeout:超时时间;
timeout为NULL,阻塞,直到监视的文件描述符有变化,返回正值;
timeout为0,纯粹的非阻塞,立即返回,如果监视的文件描述符有变化返回正值,否则返回0;
timeout>0,在timeout时间内为阻塞状态,在超时时间内如果监视的文件描述符有变化则返回正值,否则与timeout为0一样;
struct timeval {
long tv_sec;
long tv_usec;
}

// fd_set/文件描述符集使用整数数组的位域表示,即数组元素的每一位对应一个文件描述符;
// 一般操作系统中定义FD_SETSIZE声明fd_set文件描述符的最大数目,如#define FD_SETSIZE 1024;即32个元素的整数数组(元素:4Bytes 32bit)
void FD_CLR(int fd, fd_set *set); // 设置整数数组中对应文件描述符的位为0;
void FD_SET(int fd, fd_set * set); // 设置整数数组中对应文件描述符的位为1;
void FD_ZERO(fd_set *set); // 设置整数数组中所有的位为0;
int FD_ISSET(int fd, fd_set *set); // 判断整数数组中对应文件描述符的位是否为1;

参考的文章:
一切皆socket
linux raw socket
linux 网络编程入门
以上为个人笔记,如有错误还望指出,唯有时间了解爱

2017-03-02 11:15:14 forever917 阅读数 210
  • Linux Socket编程实战第1季第1部分

    课程特点: 1、手把手的实际操作过程; 2、引导学员一步步去思考; 3、网络技术方面初级的一步步进入linux socket编程的世界; 本课程是linux socket编程的一小部分,从无名套接口开始, 然后逐步深入,这应该是很多课程所没有的。 以通俗的比照讲清楚一些概念,更多的是如何一步步通过代码去实现,并辅之以一些小的项目来更好的 理解linux socket编程的技巧和方法。

    1891 人正在学习 去看看 熊健

int  socket(int protofamily, int type, int protocol);//返回sockfd描述符

调用socket()接口创建一个socket链接的时候,返回的是一个描述符,它存于协议族空间内,但是没有一个具体的地址。

        protofamily,协议族。常用的有IPV4(AF_INET), IPV6(AF_INET6), Unix域socket(AF_LOCAL/AF_UNIX)等等。协议族决定了socket的地址类型,如AF_UNIX用绝对路径,AF_INET用32位的IPV4地址和16为的端口号组合。

        type,socket类型。常见的有SOCK_RAW, SOCK_PACKET, SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET等等。当protocol为0时,type会指定默认的protocol。

        protocol,指定协议。常见的有TCP(IPPROTO_TCP)、UDP(IPPTOTO_UDP)、STCP(TPPROTO_STCP)、TIPC(IPPROTO_TIPC)。


int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//给socket()返回的描述符赋予一个特定的地址。

        sockfd,socket()返回的描述符。bind()给这个描述符绑定一个地址。

        addr,指向绑定给sockfd的协议地址。根据创建socket的时候的protofamily不同而不同。


服务端会调用bind绑定一个自身ip和端口组合的地址,以便于客户端通过这个地址来连接服务器。客户端则只需要connect()时随机生成一个即可。

socket在绑定地址的时候,必须将主机字节序转换为网络字节序。

主机字节序,不同的CPU有不同的字节序类型,其表示整数在内存中的存储顺序。

网络字节序,4个字节的值由低位到高位8个bit的顺序传输。


int listen(int sockfd, int backlog);

server在创建socket并绑定后,就会监听连接状态,使用listen来查看是否有client connect。

sockfd是监听的socket地址(已经bind绑定),backlog是最大连接数。


int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//clinet发送连接请求

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

server在listen时接受到了sever的connect请求,这时去用accept接受请求,以建立连接。accept是默认阻塞进程的,直到客户端建立连接完成后返回,accept返回的是的连接套接字。

sockfd是监听的套接字,addr是客户端的地址。


连接套接字,代表的是网络存在中的连接。

监听套接字,是listen的返回,accept的传入参数。

Server创建一个监听socket的描述符,一直存在于服务器的生命周期。内核中产生新的描述符-连接套接字使用的是和监听套接字一样的端口号。


Server和client的网络通讯读写操作:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

异常错误:EINTR表示中断,ECONNREST表示网络连接问题。


int close(int fd);//读写后关闭相应socket描述符

close将相应的socket描述字引用计数-1,当计数为0的时候Server才会发送终止连接请求FIN。close将socket描述符关闭,Server不能再去进行读写操作。Server进入FIN_WAIT2状态,client进入CLOSE_WAIT状态。close后client发送数据到Server,而Server的读写端皆以关闭。client第一次发送数据时Server返回RST表示不再接受数据,如果第二次client发送数据给Server则不能发送成功,而产生一个SIGPIPE信号给应用层,导致程序终止。故此,client应检查这种被动关闭的情况。

linux socket网络编程实例

博文 来自: geliusheng

Linux socket 编程简单例子

博文 来自: lichengyu

Linux socket API 使用实例

博文 来自: carry1314lele
没有更多推荐了,返回首页