目录
1.建立连接
(1)socket函数:建立一个套接口,类似于open,用来打开一个网络连接,如果成功则返回一个网络文件描述符(int),之后我们操作这个网络连接都通过这个网络文件描述符。
(2)bind函数:将socket建立的套接口与一个本地地址捆绑(主机地址/端口号)。
(3)listen函数:把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求。
(4)accept函数:开始接收从客户端发来的请求信息。
(5)connect函数:发起对服务器的连接请求,三次握手在此时开始。
socket函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol); //创建一个 套接字
domain:AF_INET、AF_INET6、AF_UNIX
type:SOCK_STREAM、SOCK_DGRAM
protocol: 0
返回值:
成功: 新套接字所对应文件描述符
失败: -1 errno
bind函数:
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //给socket绑定一个 地址结构 (IP+port)
sockfd: socket 函数返回值
addr: 传入参数(struct sockaddr *)&addr
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr =inet_addr(SER_ADDR_IP);
addrlen: sizeof(addr) 地址结构的大小。
返回值:
成功:0
失败:-1 errno
listen函数:
int listen(int sockfd, int backlog); //设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
sockfd: socket 函数返回值
backlog:上限数值。最大值 128.
返回值:
成功:0
失败:-1 errno
accept函数:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
sockfd: socket 函数返回值
addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
socklen_t clit_addr_len = sizeof(addr);
addrlen:传入传出。 &clit_addr_len
传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
成功:能与客户端进行数据通信的 socket 对应的文件描述。
失败: -1 , errno
connect函数:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接
sockfd: socket 函数返回值
addr:传入参数。服务器的地址结构
struct sockaddr_in srv_addr; // 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527 //跟服务器bind时设定的 port 完全一致。
ser_sockaddr.sin_addr.s_addr = inet_addr(SER_ADDR_IP); // 服务器IP地址
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1 errno
2.发送和接收
(1)send和write:都是用来发送信息。
(2)recv和read:都是用来接收信息。
3.辅助性函数
(1)inet_aton(将字符串转换成网络地址)、inet_addr(构建网络地址)、inet_ntoa(将网络地址转换成字符串)。
(2)inet_ntop(将网络地址转换成字符串)、inet_pton(将字符串转换成网络地址)。两者都兼容IPv4和IPv6。
网络字节序:
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678
大端法:(网络存储) 高位存低地址。地位存高地址。
htonl --> 本地--》网络 (IP)
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序
htons --> 本地--》网络 (port)
ntohl --> 网络--》 本地(IP)
ntohs --> 网络--》 本地(Port)
4.IP地址转换函数:
int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序
af:AF_INET、AF_INET6
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP)
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值: 成功:dst。
失败:NULL
5.表示IP地址相关数据结构
(1)都定义在netinet/in.h。
(2)struct sockaddr:这个结构体是网络编程中用来表示一个IP地址的,注意这个IP地址是兼容IPv4和IPv6的,在实际编程中这个结构体会被struct sockaddr_in或者struct sockaddr_in6。
(3)typedef uint32_t in_addr_t:网络内部用来表示IP地址的类型。
(4)struct sockaddr_in
{
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
sin_family: 地址族。一般来说是AF_INET和PF_INET
sin_port: 端口号(使用网络字节顺序)。在linux 下,端口号的范围是 0~65535, 0~1024 范围的端口号已经被系统使用或保留。
sin_addr: 存储IP地址,使用in_addr 这个数据结构。
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
sin_zero: 为了将 sockaddr_in 结构与 sockaddr 结构对齐
sockaddr地址结构: IP + port --> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6 man 7 ip
addr.sin_port = htons(9527);
int dst;
inet_pton(AF_INET, "192.157.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。
bind(fd, (struct sockaddr *)&addr, size);
6.socket编程实践
1.服务器端程序编写
(1)socket。
(2)bind。
(3)listen。
(4)accept:返回值是一个fd,accept正确返回表示已经与客户端建立TCP连接,之后需要通过这个连接对应的fd来和客户端进行读写操作。
注意:socket返回的fd叫监听fd,是用来监听客户端的,不能读写;accept返回的fd叫连接fd,用来读写。
myServer.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define SER_PORT 8889 //本地端口
#define SER_ADDR_IP "192.168.0.15" //本地IP
int main()
{
int s_fd = 0 , c_fd = 0 , ret = 0; //文件描述符
struct sockaddr_in ser_sockaddr = {0}; //服务器 sockaddr数据结构
struct sockaddr_in cli_sockaddr = {0}; //客户端 sockaddr数据结构
socklen_t cli_addr_len = 0; //客户端地址结构大小
char cli_addr_ip[1024] = {0}; //客户端IP地址
char buf[1024] = {0};
//第1步: 创建socket , 得到监听fd
s_fd = socket(AF_INET , SOCK_STREAM , 0);
if(-1 == s_fd)
{
perror("socket");
return -1;
}
printf("socket = %d\n" , s_fd);
//第2步: bind绑定s_fd和当前电脑的ip地址和端口号
ser_sockaddr.sin_family = AF_INET; // 设置地址族为IPv4
ser_sockaddr.sin_port = htons(SER_PORT); // 设置地址的端口号信息:本地--》网络 (IP)
ser_sockaddr.sin_addr.s_addr = inet_addr(SER_ADDR_IP); // 设置IP地址
//ser_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 获取本机任意有效IP
ret = bind(s_fd , (const struct sockaddr *)&ser_sockaddr, sizeof(ser_sockaddr));
if(-1 == ret)
{
perror("bind");
return -1;
}
printf("bind success!\n");
//第三步:listen监听端口
ret = listen(s_fd , 100);
if(-1 == ret)
{
perror("listen");
return -1;
}
printf("listen success!\n");
// 第四步:accept阻塞等待客户端接入
cli_addr_len = sizeof(cli_sockaddr); //获取客户端地址结构大小
c_fd = accept(s_fd , (struct sockaddr *)&cli_sockaddr , &cli_addr_len);
printf("连接已建立,client fd = %d\n" , c_fd);
//网络字节序 ---> 本地字节序(string IP)
printf("client ip:%s port:%d\n",
inet_ntop(AF_INET, &cli_sockaddr.sin_addr.s_addr, cli_addr_ip, sizeof(cli_addr_ip)),
ntohs(cli_sockaddr.sin_port)); // 根据accept传出参数,获取客户端 ip 和 port
//小写 --> 大写 or 大写 --> 小写
while(1)
{
memset(buf , 0 , sizeof(buf));
ret = read(c_fd , buf , sizeof(buf)); //阻塞读取
if(ret == 0) break; //客户端断开连接
write(STDOUT_FILENO , buf , ret);
for(int i = 0 ; i < ret ; i++)
{
if(buf[i] >= 'a' && buf[i] <= 'z')
{
buf[i] -= 32 ;
}
else if(buf[i] >= 'A' && buf[i] <= 'Z')
{
buf[i] += 32 ;
}
}
printf("\n");
write(c_fd , buf , ret);
}
close(s_fd);
close(c_fd);
return 0;
}
2.客户端程序的编写
(1)socket。
(2)connect。
概念:端口号,实质就是一个数字编号,用来在一台主机的操作系统中唯一地标识一个能上网的进程。网络上传输的每一个数据包都包含了发送方和接收方的IP地址和端口号。
myClient.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define SER_PORT 8888 // 服务器开放给我们的IP地址和端口号
#define SER_ADDR_IP "192.168.0.100"
int main(void)
{
int s_fd = 0 , ret = 0; //文件描述符
struct sockaddr_in ser_sockaddr = {0}; //服务器 sockaddr数据结构
char buf[1024] = {0};
//第1步: 创建socket , 得到监听fd
s_fd = socket(AF_INET , SOCK_STREAM , 0);
if(-1 == s_fd)
{
perror("socket");
return -1;
}
printf("socket = %d\n" , s_fd);
// 第2步:connect连接服务器
ser_sockaddr.sin_family = AF_INET; // 设置地址族为IPv4
ser_sockaddr.sin_port = htons(SER_PORT); // 设置地址的端口号信息:本地--》网络 (IP)
ser_sockaddr.sin_addr.s_addr = inet_addr(SER_ADDR_IP); // 设置IP地址
ret = connect(s_fd, (const struct sockaddr *)&ser_sockaddr, sizeof(ser_sockaddr));
if (ret < 0)
{
perror("listen");
return -1;
}
printf("connect successfully.\n");
//小写 --> 大写 or 大写 --> 小写
while(1)
{
memset(buf , 0 , sizeof(buf));
gets(buf);
write(s_fd , buf , sizeof(buf));
ret = read(s_fd , buf , sizeof(buf)); //阻塞读取
if(ret == 0) break; //客户端断开连接
write(STDOUT_FILENO , buf , ret);
printf("\n");
}
close(s_fd);
return 0;
}
3.运行效果:
7.出错处理封装函数
上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。
为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c:
wrap.c
#include "wrap.h"
void perr_exit(const char *s)
{
perror(s);
exit(-1);
}
int Socket(int domain, int type, int protocol)
{
int n;
if ((n = socket(domain, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ((n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR)) //被信号打断
goto again;
else
perr_exit("accept error");
}
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
n = connect(fd, sa, salen);
if (n < 0) {
perr_exit("connect error");
}
return n;
}
ssize_t Read(int fd, void *buf, size_t count)
{
ssize_t n;
again:
if ( (n = read(fd, buf, count)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *buf, size_t count)
{
ssize_t n;
again:
if ((n = write(fd, buf, count)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
/*参三: 应该读取的字节数*/ //socket 4096 readn(cfd, buf, 4096) nleft = 4096-1500
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft; //usigned int 剩余未读取的字节数
ssize_t nread; //int 实际读到的字节数
char *ptr;
ptr = vptr;
nleft = n; //n 未读取字节数 , 需要读取的数据量
while (nleft > 0)
{
if ((nread = read(fd, ptr, nleft)) < 0) //阻塞等待数据
{
if (errno == EINTR) //被信号杀死
nread = 0;
else //读出错
return -1;
} else if (nread == 0) //没有数据了
return 0;
nleft -= nread; //nleft = nleft - nread
ptr += nread; //更新缓冲区指针
}
return n - nleft; //返回没读够的字节数
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ( (nwritten = write(fd, ptr, nleft)) <= 0)
{
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0)
{ //"hello\n"
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
/*readline --- fgets*/
//传出参数 vptr
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++)
{
if ((rc = my_read(fd, &c)) == 1)
{ //ptr[] = hello\n
*ptr++ = c;
if (c == '\n')
break;
}
else if (rc == 0)
{
*ptr = 0;
return n-1;
} else
return -1;
}
*ptr = 0;
return n;
}
wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
void perr_exit(const char *s);
int Socket(int domain, int type, int protocol);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
ssize_t Read(int fd, void *buf, size_t count);
ssize_t Write(int fd, const void *buf, size_t count);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif