首页 > 代码库 > linux之socket编程总结

linux之socket编程总结

前言

       在TCP/IP协议中,网络层的“ip地址”可以唯一标识网络中的主机,传输层的“协议和端口”可以唯一标识主机中的进程。这样利用三元组(ip地址,协议,端口)就可以唯一标识网络的进程,网络中的进程通信就可以利用这个标志与其它程序进行交互。在这之中大部分应用都是通过socket实现的。

socket 

    通常称作“套接字”,用于描述ip地址和端口,是一个通信链的句柄。是使用Uinx文件描述符和其它程序通迅的方式。本质上就是一组接口,使编程简单化。

Internet套接字

流套接字(SOCK_STREAM):可靠的双向通迅的数据流。无错误传递,有自己的错误控制,使用TCP协议,只能读取此协议数据。
数据报套接字(SOCK_DGRAM):提供一种无连接的服务。不保证数据传输的可靠性,需自行处理,使用UDP协议,只能读取此协议数据。
原始套接字(SOCK_RAW):可以读写内核没有处理的ip数据包,如果需要访问其他协议(非TCP/UDP)发送数据必须使用原始套接字。

socket描述符

        本质上就是一个整数,类型:int。当进程要创建套接字时,系统就提供一个小的整数作为描述符来标识这个套接字。然后进程利用该描述符来完成某种操作(如文件的读写)。


socket数据结构

       套接字的内部数据结构包含很多字段,但系统创建后,大多数字段没有填写,需要进行具体配置才能使用。

技术分享

struct sockaddr {
unsigned short sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /*14 字节协议地址*/
};

struct sockaddr_in ("in" 代表 "Internet"。)
struct sockaddr_in {
short int sin_family; /* 通信类型 */
unsigned short int sin_port; /* 端口 */
struct in_addr sin_addr; /* Internet 地址 */
unsigned char sin_zero[8]; /* 为了保证结构体struct sockaddr_in的大小和结构体struct sockaddr的大小相等*/
};

struct in_addr {
unsigned long s_addr;
};

        sa_family可以是各种类型,常用的是“AF_INET”。sa_data包含套接字中的目标地址和端口信息。

        struct sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式,二者长度一样,都是16个字节。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
注意 :sin_zero (它被加入到这个结构,并且长度和 struct sockaddr一样) 应该使用函数 bzero() 或 memset() 来全部置零。 同时,一个指向 sockaddr_in 结构体的指针也可以被指向结构体 sockaddr 并且代替它。这样的话即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 structsockaddr_in,并且在最后转换。

        注意 sin_family 和 struct sockaddr 中的 sa_family 一致并能够设置为 "AF_INET"。最后,sin_port 和 sin_addr 必须是网络字节顺序 sin_family必须是本机字节顺序。因为sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是sin_family 域只是被内核(kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时,sin_family 没有发送到网络上,它们可以是本机字节顺序。

        通常的用法是:

int sockfd;
struct sockaddr_in my_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0); 

my_addr.sin_family = AF_INET; /* 主机字节序 */
my_addr.sin_port = htons(MYPORT); /* short, 网络字节序 */

my_addr.sin_addr.s_addr = inet_addr("192.168.0.1");

bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */
//memset(&my_addr.sin_zero, 0, 8);

bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));


网络与本机字节顺序的转换

       主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。


       网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。

htons()--"Host to Network Short" //将short类型的值从主机字节序转换为网络字节序(short两个字节,即16位值)

htonl()--"Host to Network Long" (long四个字节,即32位值)

ntohs()--"Network to Host Short"

ntohl()--"Network to Host Long"

ip地址处理

        inet_addr():将 IP 地址从 点数格式转换成无符号长整型。

        使用方法:

sockaddr.sin_addr.s_addr = inet_addr("192.168.1.10");       //将IP地址字符串转换为long类型的网络字节序,发生错误时返回-1。

inet_ntoa();         //将long类型的网络字节序转换成IP地址字符串。

注意,inet_addr()返回的地址已经是网络字节格式,所以你无需再调用函数 htonl()。

使用方法:

printf("%s",inet_ntoa(sockaddr.sin_addr.s_addr));

注意, inet_ntoa()返回的是一个指向一个字符的 指针。它是一个由 inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的 IP 地址。


gethostname():获取程序所运行的机器的主机名字。

#include <unistd.h>

int gethostname(char *hostname, size_t size);

hostname 是一个字符数组指针,它将在函数返回时保存主机名。size 是 hostname 数组的字节长度。

函数调用成功时返回 0,失败时返回 -1,并设置 errno。


gethostbyname():获取机器的ip地址

#include <netdb.h>

struct hostent *gethostbyname(const char *name);

其数据结构如下:

struct hostent {

char *h_name;  - 地址的正式名称。

char **h_aliases;  - 空字节-地址的预备名称的指针。

int h_addrtype;  -地址类型; 通常是 AF_INET。

int h_length; - 地址的比特长度。

char **h_addr_list;  - 零字节-主机网络地址指针。网络字节顺序。

};

#define h_addr h_addr_list[0]  //  - h_addr_list 中的第一地址。

gethostbyname() 成功时返回一个指向结构体 hostent 的指针,或者 是个空 (NULL) 指针。(但是和以前不同,不设置errno,h_errno 设置错误信息而是使用herror()。)

以下为使用例子:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
struct hostent *h;
if (argc != 2) { /* 检查命令行 */
fprintf(stderr,"usage: getip address\n");
exit(1);
}
if ((h=gethostbyname(argv[1])) == NULL) { /* 取得地址信息 */
herror("gethostbyname");
exit(1);
}
printf("Host name : %s\n", h->h_name);
printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr))); //h->h_addr 是一个 char *, 但是 inet_ntoa() 需要的是 struct in_addr。故转换h->h_addr 成 struct in_addr *,然后得到数据。
return 0;
}

socket基本函数

socket():

#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain, int type, int protocol); //返回描述符,错误时返回-1

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。

type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。

protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议。

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。


bind():

#include <sys/types.h>

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, int addrlen); //在错误的时候返回-1,并且设置全局错误变量 errno。

sockfd 是调用 socket 返回的文件描述符。my_addr 是指向数据结构 struct sockaddr 的指针,它保存你的地址(即端口和IP 地址) 信息。 addrlen 设置为 sizeof(struct sockaddr)。这里有些工作系统会自己处理:

my_addr.sin_port = 0; /* 系统随机选择一个没有使用的端口*/

my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的 IP 地址 ,INADDR_ANY实际上为0,不需要转网络字节顺序*/

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。另外,不要采用小于 1024 的端口号。所有小于 1024 的端口号都被系统保留!你可以选择从 1024 到 65535 的端口(如果它们没有被别的程序使用的话)。


listen()、connect():

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

#include <sys/types.h>

#include <sys/socket.h>

int listen(int sockfd, int backlog); //在发生错误的时候返回-1,并设置全局错误变量 errno。

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


int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //在错误的时候返回-1,并 设置全局错误变量 errno。

connect函数的第一个参数是系统调用 socket()返回的套接字文件描述符,第二参数为服务器的socket地址,保存着目的地端口和 IP 地址的数据结构 struct sockaddr,第三个参数为socket地址的长度,通常设置为sizeof(struct sockaddr)。客户端通过调用connect函数来建立与TCP服务器的连接。


accept():

#include <sys/socket.h>

int accept(int sockfd, void *addr, int *addrlen); //在错误时返回-1,并设置全局错误变量 errno。

sockfd是套接字描述符。addr 是指向局部的数据结构 sockaddr_in 的指针。addrlen 是接受addr的结构的大小的,设置为 sizeof(struct sockaddr_in),也可以被设置为NULL。

注意:

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

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

      连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号。


数据发送

send() 和 recv()函数:用于流式套接字或者数据报套接字的通讯。

int send(int sockfd, const void *msg, int len, int flags); //返回实际发送的字节数(可能小于想发送的字节数)在错误的时候返回-1,并设置 errno。

sockfd 是你想发送数据的套接字描述符(或者是调用 socket() 或者是 accept() 返回的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就可以了。

int recv(int sockfd, void *buf, int len, unsigned int flags); //返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。

sockfd 是要读的套接字描述符。buf 是要读的信息的缓冲。len 是缓 冲的最大长度。flags 可以设置为 0。


sendto() 和 recvfrom()函数:用于无连接数据报套接字;

int sendto(int sockfd, const void *msg, int len, unsigned int flags,const struct sockaddr *to, int tolen);

 to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为sizeof(struct sockaddr)。其余的与send()一样。

int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

from 是一个指向局部数据结构 struct sockaddr 的指针,

它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。其余的与recv()一样。


close()和 shutdown():关闭套接字描述符

#include <unistd.h>

int close(int fd);

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。


int shutdown(int sockfd, int how);//成功时返回 0,失败时返回 -1,同时设置 errno。

sockfd 是你想要关闭的套接字文件描述复。how 的值是下面的其中之 一:

0 - 不允许接受

1 - 不允许发送

2 - 不允许发送和接受(和 close() 一样)


阻塞

       阻塞就是在执行设备操作时,若不能获得资源,则进程挂起直到满足可操作条件再进行操作。很多函数都利用阻塞。像accept(),recv() 函数。当第一次调用 socket() 建 立套接字描述符的时候,内核就将它设置为阻塞。如果不想套接字阻塞, 可以调用函数 fcntl()、select(),具体的以后再研究。


Socket编程实例:

       服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。当个客户端初始化一个Socket,然后连接服务器(connect)时,如果连接成功,刚客户端与服务器端的连接就建立成功了。接下来就可以进行数据的发送与接收。其流程如下:

技术分享

socket简单测试程序

服务器代码:
#include <netinet/in.h>    // for sockaddr_in
#include <sys/types.h>    // for socket
#include <sys/socket.h>    // for socket
#include <stdio.h>        // for printf 、perror
#include <stdlib.h>        // for exit  、perror
#include <string.h>        // for bzero

#define SERVER_PORT    6666 
#define BUFFER_SIZE 1024
#define FILE_NAME_SIZE 512
 
int main(int argc, char **argv)
{
	int server_socket;
    struct sockaddr_in server_addr;
	
    bzero(&server_addr,sizeof(server_addr)); 
	
	//设置通信类型为ipv4,端口号,ip地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htons(INADDR_ANY);
    server_addr.sin_port = htons(SERVER_PORT);
 
    //创建用于internet的流协议(TCP)的socket
	if (-1 == (server_socket = socket(AF_INET, SOCK_STREAM, 0)))
	{
		perror("Create Socket Failed:");
		return -1;
	}
 
    //将套接字与服务器地址绑定
    if( -1 == (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr) )))
    {
        perror("Server Bind Port Failed!"); 
        return -1;
    }
 
    //监听连接请求,监听队列长度为5
    if ( -1 == (listen(server_socket, 5)) )
    {
        perror("Server Listen Failed!"); 
        return -1;
    }
	
    while (1) //服务器端要一直运行
    {
        //定义客户端的socket描述符
		int client_socket;
        struct sockaddr_in client_addr;
        int sock_length;
        sock_length = sizeof(client_addr);
 
        /*
         * 接受一个到server_socket代表的socket的一个连接
         *accept函数返回一个新的socket,这个socket(client_socket)用于同连接到的客户的通信
         *accept函数把连接到的客户端信息填写到客户端的socket地址结构client_addr中
        */
        if (-1 == (client_socket = accept(server_socket,(struct sockaddr*)&client_addr,&sock_length)))
        {
            perror("Server Accept Failed!\n");
            return -1;
        }
        printf("Conecting........OK!\n");
         
        char buffer[BUFFER_SIZE];
        bzero(buffer, BUFFER_SIZE);
        int length;
		
		//接收与发送数据
        while ( length = (recv(client_socket, buffer, BUFFER_SIZE, 0)))
        {
            if (-1 == length)
            {
                perror("Server Recieve Data Failed!\n");
                break;
            }
			
			buffer[BUFFER_SIZE] = '\0';
            printf("%s\n", buffer);
         //   bzero(buffer, BUFFER_SIZE);

			if ( -1 == send(client_socket, buffer, BUFFER_SIZE, 0) )
			{
				perror("Write Failed!\n");
				return -1;
			}
        }
        //关闭与客户端的连接
        close(client_socket);
    }

    //关闭监听用的socket
    close(server_socket);
    return 0;
}

客户端代码:
#include <netinet/in.h>    // for sockaddr_in
#include <sys/types.h>    // for socket
#include <sys/socket.h>    // for socket
#include <stdio.h>        // for printf
#include <stdlib.h>        // for exit
#include <string.h>        // for bzero
 
#define SERVER_PORT    6666 
#define BUFFER_SIZE 1024
 
int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("Usage: ./%s ServerIPAddress\n",argv[0]);
        exit(1);
    }
 
	int client_socket;
    struct sockaddr_in client_addr;
	
    bzero(&client_addr,sizeof(client_addr)); 
	
	//设置通信类型为ipv4,端口号(0表示系统自动分配),ip地址(INADDR_ANY自动获取)
    client_addr.sin_family = AF_INET;   
    client_addr.sin_port = htons(SERVER_PORT);
	if(inet_aton(argv[1],&client_addr.sin_addr) == 0) //服务器的IP地址来自程序的参数
    {
        printf("Server IP Address Error!\n");
        return -1;
    }
	
    //创建用于internet的流协议(TCP)socket
    if ( -1 == (client_socket = socket(AF_INET, SOCK_STREAM, 0)))
	{
		perror("Create Socket Failed:");
		return -1;
	}

    //向服务器发起连接,连接成功后client_socket代表了客户机和服务器的一个socket连接
    if(-1 == connect(client_socket, (struct sockaddr*)&client_addr, sizeof(struct sockaddr)))
    {
        printf("Can Not Connect To %s!\n", argv[1]);
        return -1;
    }
	printf("Conect to %s ..........OK!\n", argv[1]);
     
    char buffer[BUFFER_SIZE];
	int  length;
	
    bzero(buffer,BUFFER_SIZE);
	
	//向服务器发送数据并打印
    while (1)
	{
		printf("Enter data to send: ");
		scanf("%s", buffer);
		
		if (!strcmp(buffer, "quit"))
		{
			break;
		}
		
		if ( -1 == (length = send(client_socket, buffer, strlen(buffer), 0)))
		{
			printf("Send data failed!\n");
		}
		else
		{
			buffer[length] = '\0';
			printf("Send Data: %s \n", buffer);
		}
		
		bzero(buffer, BUFFER_SIZE);
	}
     
    close(client_socket);

    return 0;
}

只是一个小小的测试程序,关于数据的传送也只是简单的从键盘输入。


linux之socket编程总结