linux网络编程之socket记录
socket可以看成是用户进程与内核网络协议栈的编程接口。
socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信。

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及以后要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同
比如说有ipv4和ipv6
ipv4和ipv6的地址定义在netinet/in.h里面
ipv4地址用sockaddr_in结构体表示,包含16bit端口号和32bit ip地址
ipv6地址用sockaddr_in6结构体表示

而socket地址结构体开头都是相同的,对于一些unix实现来说,前8位表示整个结构体的长度,后8位表示地址类型,而linux前2个字节都是地址类型,没有长度字段。地址类型的定义是常数:IPV4的定义是AF_INET,IPV6:AF_INET6,Unix Domain Socket:AF_UNIX
所以说实际上只要知道sockaddr结构体的首地址不需要知道具体哪种类型的结构体就可以根据地址字段确定结构体内容。比如bind accept connect等函数都可以接受不同类型的sockaddr,应该用void* 作为参数格式但是很遗憾c语言没有这么做而是用的(struct sockaddr*)的参数类型,所以说ipv4的结构体传参时候要强转


然后网络上的字节序是和主机的一般相反的主机字节序一般是小端,网络字节序是大端
所以有一系列转换函数:
#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,l是long,s是short
不用管主机字节序,反正他就是给你转换,如果主机和网络一个字节序他就不动
#include<stdio.h>
#include<arpa/inet.h>
int main(void)
{
unsigned int x = 0x12345678;
unsigned char *p = (unsigned char *)&x;
printf("%x %x %x %x\n", p[0], p[1], p[2], p[3]);
unsigned int y = htonl(x);
p = (unsigned char *)&y;
printf("%x %x %x %x\n", p[0], p[1], p[2], p[3]);
return 0;
}

然后地址转换如何把字符串转换成in_addr格式(in_addr是结构体里面有唯一成员s_addr是uint32)
函数如下
#include<arpa/inet.h>
int inet_aton(const char*strptr,struct in_addr*addrptr);
in_addr_t inet_addr(const char*strptr);
int inet_pton(int family,const *strptr,void*addrptr);//这个支持ipv6
反过来in_addr转换成字符串
#include<arpa/inet.h>
char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family,const void*addrptr,char*strptr,size_t len);
演示:
#include<stdio.h>
#include<arpa/inet.h>
int main(void)
{
unsigned int addr = inet_addr("192.168.0.100"); //转换后是网络字节序(大端)
printf("add=%u\n", ntohl(addr));
struct in_addr ipaddr;
ipaddr.s_addr = addr;
printf("%s\n", inet_ntoa(ipaddr));
return 0;
}
输出

套接字类型:
SOCK_STREAM流式套接字 面向连接的 可靠的 无差错无重复 按发送顺序接收
SOCK_DGRAM数据报式套接字 无连接 不提供无错保证 数据可能丢失或重复,并且接受顺序可能混乱
SOCK_RAW原始套接字
socket通信的基本流程:
首先服务器调用socket() bind() listen()完成初始化,服务器调用accept()阻塞等待处于监听端口的状态。客户端调用socket()初始化,调用connect()发送SYN段之后阻塞等待服务器应答。服务器回答SYN-ACK段,客户端收到后从connect()返回,同时应答ACK段,服务器收到ACK段之后从accept()返回
这就建立了连接
然后服务器调用read(),读socket就像读管道一样,没有数据就阻塞,如果客户端调用write发送,那么服务器的read()收到并且返回,就收到了客户端的请求,而客户端如果此时调用read()阻塞等待服务器的回答,那么服务器可以调用write()把处理结果发送给客户端,然后再次调用read()阻塞等待下一条请求
如果客户端close()关闭连接,那么服务器的read()返回0 然后服务器也调用close()
任何一方调用close()双方的连接的两个传输方向都关闭,但是如果一方shutdown(),那么处于半关闭状态还能接收对方的数据
下面是实例:
这个是服务器的代码
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main(void)
{
int listenfd; //被动套接字(文件描述符),即只可以accept
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
// listenfd = socket(AF_INET, SOCK_STREAM, 0)
ERR_EXIT("socket error");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
/* inet_aton("127.0.0.1", &servaddr.sin_addr); */
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt error");
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind error");
if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
ERR_EXIT("listen error");
struct sockaddr_in peeraddr; //传出参数
socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值
int conn; // 已连接套接字(变为主动套接字,即可以主动connect)
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
ERR_EXIT("accept error");
printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}
close(conn);
close(listenfd);
return 0;
}
socket bind listen accept都在<sys/socket.h>
int socket(int family,int type,int protocol);
family是地址类型,IPV4是AF_INET
type是套接字类型,TCP是SOCK_STREAM,UDP是SOCK_DGRAM,然后protocol如果是TCP可以填IPPROTO_TCP
但是实际上填0就默认从前两个参数确定了
功能:打开一个网络通讯端口,成功返回文件描述符fd,失败返回-1
int bind(int sockfd,const struct sockaddr*myaddr,socklen_t addrlen);
把sockfd和myaddr绑定在一起,使得sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号实际上不管是客户端还是服务器都不是必须的,因为会自动分配端口号,但是服务器自动分配端口号就不好连接了
成功返回0失败返回-1
addrlen是第二个参数结构体的长度,第二个参数接受地址结构体时候要强制转换
那么这个例子里面servaddr就是ipv4的结构体,sin_family是AF_INET,sin_port是端口号,但是要转换成大端并且是两个字节,sin_addr.s_addr是ip地址,填127.0.0.1就可以,这里面有常量INADDR_ANY,都是任何地址的意思。
int listen(int sockfd,int backlog);
这个函数不是真的阻塞监听的意思,而是声明sockfd进入一个状态,这个状态是监听状态(当然就是等待连接的状态),而真正要连接需要accept()函数,accept建立连接之后会返回新的文件描述符,如果有很多客户端发起连接但是一次accept最多连接一个,所以最多允许backlog个客户端处于连接等待状态,如果接受到更多的连接请求就忽略。成功返回0,失败返回-1
这个例子里面使用了常量SOMAXCONN作为backlog,应该是一个上限了
int accept(int sockfd,struct sockaddr*cliaddr,socklen_t *addrlen);
客户端使用connect连接服务器进行三次握手完成后,服务器的accept()接受连接,如果调用accept()时候还没有客户端的连接请求就阻塞等待。cliaddr是传出参数,传出连接的客户端的地址和端口号,addrlen是一个传入传出参数,传入的是cliaddr的缓冲区长度,传出的是客户端地址结构体的实际长度(有可能没满),如果cliaddr addrlen传null也可以,就代表不需要客户端的地址和端口号。
然后函数返回的是连接的文件描述符,这样可以直接从这个文件描述符读写(也就是通信)
而后面是阻塞的读取客户端发送的内容,然后原封不动输出并且发送回去。
下面看客户端:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
// listenfd = socket(AF_INET, SOCK_STREAM, 0)
ERR_EXIT("socket error");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/* inet_aton("127.0.0.1", &servaddr.sin_addr); */
if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect error");
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sock, sendbuf, strlen(sendbuf));
read(sock, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
return 0;
}
客户端同样是要创建一个socket
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
这个PF和AF是一样的
然后这里面不用bind因为你也不需要监听,直接connect连接就行
int connect(int sockfd,const struct sockaddr*servaddr,socklen_t addrlen);
在<sys/socket.h>里面
这里面连接的是目标服务器的地址和端口
连接之后这个sockfd就是和服务器通信的文件描述符了,而不像accept会开一个新的
成功返回0,失败返回-1
然后客户端就从stdin读取buf,然后写入到sock文件描述符里面,就是发送给服务器,然后从服务器接收并且原封不动的输出。
现在有一个问题,如果把server强制终止了然后再运行server时候bind会报错说地址被占用。
这是因为server终止了但是tcp连接还在,server终止时候socket描述符会自动关闭并且发送FIN段给client,client收到FIN之后处于CLOSE_WAIT状态,但是client没有终止也没有关闭socket描述符,因此不会发送FIN给server,所以server处于FIN_WAIT2状态。所以同一个端口server不能反复监听
如果这时候终止client,那么socket描述符关闭,client发送FIN段,server的TCP不会马上关闭。因为TCP规定主动关闭连接的一方要处于TIME_WAIT状态等待两个msl的时间才回到closed状态。所以在TIME_WAIT期间再监听server端口也不行,一般要等0.5min-2min.
但是监听的文件描述符和连接的文件描述符毕竟不一样,是和客户端连接的TCP没有完全断开,但是我们重新监听的是listenfd,实际上和客户端的IP可以不一样(比如说监听0.0.0.0),所以只要允许创建端口号相同IP不同的多个socket描述符就不会被占用
int on=1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))
解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1
黄粱一梦,终是一空
本文来自博客园,作者:hicode002,转载请注明原文链接:https://www.cnblogs.com/hicode002/p/19486663

浙公网安备 33010602011771号